Skip to main content

Overview

Refs provide a way to access DOM elements or store mutable values that persist across renders. Portals allow you to render components outside of the parent component’s DOM hierarchy.

useRef Hook

Access DOM elements and store mutable values

forwardRef

Pass refs through components to children

useImperativeHandle

Expose custom component APIs to parent components

Portals

Render components outside the parent DOM tree

Why Use Refs?

Refs are useful when you need to:
1

Access DOM elements

Directly manipulate DOM elements (focus, scroll, measure)
2

Store mutable values

Keep values that don’t trigger re-renders when changed
3

Persist across renders

Maintain values that survive component re-renders
4

Integrate with third-party libraries

Work with libraries that require direct DOM access

useRef Hook

Basic DOM Access

The most common use case for refs is accessing DOM elements.
import { useState, useRef } from 'react';

export default function Player() {
  const playerName = useRef();
  const [enteredPlayerName, setEnteredPlayerName] = useState(null);
  
  function handleClick() {
    setEnteredPlayerName(playerName.current.value);
  }

  return (
    <section id="player">
      <h2>Welcome {enteredPlayerName ?? 'unknown entity'}</h2>
      <p>
        <input
          ref={playerName}
          type="text"
        />
        <button onClick={handleClick}>Set Name</button>
      </p>
    </section>
  );
}
The current property of a ref contains the actual DOM element after React mounts it. Always access DOM elements via ref.current.

Storing Mutable Values

Refs can store any mutable value that shouldn’t trigger re-renders.
import { useState, useRef } from 'react';

export default function TimerChallenge({ title, targetTime }) {
  const timer = useRef();
  const [timerStarted, setTimerStarted] = useState(false);
  const [timerExpired, setTimerExpired] = useState(false);

  function handleStart() {
    timer.current = setTimeout(() => {
      setTimerExpired(true);
    }, targetTime * 1000);

    setTimerStarted(true);
  }

  function handleStop() {
    clearTimeout(timer.current);
  }

  return (
    <section className="challenge">
      <h2>{title}</h2>
      {timerExpired && <p>You lost!</p>}
      <p className="challenge-time">
        {targetTime} second{targetTime > 1 ? 's' : ''}
      </p>
      <p>
        <button onClick={timerStarted ? handleStop : handleStart}>
          {timerStarted ? 'Stop' : 'Start'} Challenge
        </button>
      </p>
      <p className={timerStarted ? 'active' : undefined}>
        {timerStarted ? 'Time is running...' : 'Timer inactive'}
      </p>
    </section>
  );
}
Don’t use refs for values that should trigger re-renders. Use state instead. Changing a ref’s value doesn’t cause the component to re-render.

State vs Refs

const [value, setValue] = useState(0);
Characteristics:
  • Triggers re-render when updated
  • Asynchronous updates
  • Immutable update pattern
  • Use for UI-related data
Example use cases:
  • Form input values displayed in UI
  • Toggle states (open/closed)
  • Lists, counters, any displayed data

forwardRef

By default, you can’t pass refs to functional components. forwardRef enables this.

Basic Usage

import { forwardRef } from 'react';

const ResultModal = forwardRef(function ResultModal(
  { result, targetTime },
  ref
) {
  return (
    <dialog ref={ref} className="result-modal">
      <h2>You {result}</h2>
      <p>
        The target time was <strong>{targetTime} seconds.</strong>
      </p>
      <p>
        You stopped the timer with <strong>X seconds left.</strong>
      </p>
      <form method="dialog">
        <button>Close</button>
      </form>
    </dialog>
  );
});

export default ResultModal;
forwardRef accepts a render function that receives props and ref as separate arguments. The component name should be provided as the function name for better debugging.

useImperativeHandle

Customize the ref value exposed to parent components.
import { forwardRef, useImperativeHandle, useRef } from 'react';

const ResultModal = forwardRef(function ResultModal(
  { result, targetTime },
  ref
) {
  const dialog = useRef();

  useImperativeHandle(ref, () => {
    return {
      open() {
        dialog.current.showModal();
      }
    };
  });

  return (
    <dialog ref={dialog} className="result-modal">
      <h2>You {result}</h2>
      <p>
        The target time was <strong>{targetTime} seconds.</strong>
      </p>
      <p>
        You stopped the timer with <strong>X seconds left.</strong>
      </p>
      <form method="dialog">
        <button>Close</button>
      </form>
    </dialog>
  );
});

export default ResultModal;
useImperativeHandle is perfect for creating clean, controlled APIs for your components while hiding implementation details from parent components.

Benefits of useImperativeHandle

  • Hide internal implementation details
  • Expose only necessary methods
  • Prevent direct DOM manipulation by parents
  • Create a clear component API
  • Add validation or logic before DOM operations
  • Combine multiple operations into one method
  • Provide custom methods beyond native DOM APIs
  • Maintain control over component behavior
  • Change internal implementation without breaking parent components
  • Create self-documenting component interfaces
  • Easier to test and debug

Portals

Portals let you render components outside the parent component’s DOM hierarchy.

Why Use Portals?

Modals & Dialogs

Render modals at document root to avoid z-index issues

Tooltips

Position tooltips correctly regardless of parent overflow

Dropdowns

Prevent clipping by parent containers

Notifications

Display notifications at a consistent location

Creating a Portal

1

Add a portal root to HTML

index.html
<body>
  <div id="root"></div>
  <div id="modal"></div>
</body>
2

Import createPortal

import { createPortal } from 'react-dom';
3

Render content in the portal

return createPortal(
  <YourComponent />,
  document.getElementById('modal')
);

Basic Portal Example

import { createPortal } from 'react-dom';

export default function Modal({ title, children, onClose }) {
  return createPortal(
    <>
      <div className="backdrop" onClick={onClose} />
      <dialog open className="modal">
        <h2>{title}</h2>
        {children}
      </dialog>
    </>,
    document.getElementById('modal')
  );
}

Advanced Portal with Refs

Combine portals with refs for maximum control.
import { forwardRef, useImperativeHandle, useRef } from 'react';
import { createPortal } from 'react-dom';

const ResultModal = forwardRef(function ResultModal(
  { targetTime, remainingTime, onReset },
  ref
) {
  const dialog = useRef();

  const userLost = remainingTime <= 0;
  const formattedRemainingTime = (remainingTime / 1000).toFixed(2);
  const score = Math.round((1 - remainingTime / (targetTime * 1000)) * 100);

  useImperativeHandle(ref, () => {
    return {
      open() {
        dialog.current.showModal();
      },
    };
  });

  return createPortal(
    <dialog ref={dialog} className="result-modal">
      {userLost && <h2>You lost</h2>}
      {!userLost && <h2>Your Score: {score}</h2>}
      <p>
        The target time was <strong>{targetTime} seconds.</strong>
      </p>
      <p>
        You stopped the timer with{' '}
        <strong>{formattedRemainingTime} seconds left.</strong>
      </p>
      <form method="dialog" onSubmit={onReset}>
        <button>Close</button>
      </form>
    </dialog>,
    document.getElementById('modal')
  );
});

export default ResultModal;
Even though portals render in a different DOM location, they still behave like normal React children in terms of event bubbling and context.

Event Bubbling with Portals

Portals maintain React’s event bubbling behavior.
function Parent() {
  function handleClick() {
    console.log('Clicked in parent!');
  }

  return (
    <div onClick={handleClick}>
      <button>Regular Button</button>
      <Modal>
        <button>Portal Button</button>
      </Modal>
    </div>
  );
}
Clicking either button will trigger the parent’s onClick handler, even though the Modal renders via a portal outside the parent’s DOM tree.

Best Practices

DO use refs for:
  • Managing focus, text selection, or media playback
  • Triggering imperative animations
  • Integrating with third-party DOM libraries
  • Storing timer IDs, previous values, or other non-UI data
DON’T use refs for:
  • Anything that should trigger a re-render
  • Values displayed in the UI (use state instead)
  • Communication between components (use props/context)
  • Refs are null during the initial render
  • Access ref.current only after the component mounts
  • Use useEffect if you need to run code when a ref is set
  • Never read or write refs during rendering
  • Always provide a portal root in your HTML
  • Use portals for modals, tooltips, and overlays
  • Remember that CSS inheritance doesn’t cross portal boundaries
  • Event handlers still work as if the portal was in the React tree
  • Use meaningful names for forwarded components
  • Combine with useImperativeHandle to expose controlled APIs
  • Don’t overuse - not every component needs to forward refs
  • Document what ref methods are available to consumers

Common Patterns

import { useRef, useEffect } from 'react';

function SearchInput() {
  const inputRef = useRef();

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} type="text" />;
}

Styling

Learn about different styling approaches

Animations

Add smooth transitions to your portals and modals

Build docs developers (and LLMs) love