Advanced25 min read

Refs & the DOM

Learn how to use useRef to access DOM elements directly, store mutable values without triggering re-renders, and understand when to use refs versus state in your React applications.

What Are Refs?

In React, the standard way to interact with the DOM is through state and props — you describe what the UI should look like, and React handles the DOM updates. But sometimes you need to step outside this declarative model and interact with a DOM element directly.

Common scenarios:

  • Focusing an input after it mounts or after a user action
  • Measuring an element's size or position
  • Integrating with third-party DOM libraries (video players, chart libraries, etc.)
  • Triggering animations imperatively
  • Scrolling to a specific element

For these cases, React provides the useRef hook. A ref (short for reference) is a way to hold a reference to a value that persists across renders but does not trigger a re-render when it changes.

jsx
const myRef = React.useRef(initialValue);

useRef returns an object with a single property: current. The object itself is stable (same object every render), but you can freely read and write myRef.current.

javascript
const myRef = React.useRef(42);
console.log(myRef.current); // 42
myRef.current = 99;
console.log(myRef.current); // 99 — no re-render triggered!

This is fundamentally different from useState: changing a ref's .current value does not cause the component to re-render.

useRef for DOM Access

The most common use of useRef is to get a reference to a DOM element. You do this by passing the ref to an element's ref attribute:

jsx
function TextInput() {
  const inputRef = React.useRef(null);

  function handleClick() {
    // Access the actual DOM element
    inputRef.current.focus();
  }

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Type here..." />
      <button onClick={handleClick}>Focus the input</button>
    </div>
  );
}

Here is what happens step by step:

  1. React.useRef(null) creates a ref object: { current: null }
  2. The ref={inputRef} attribute tells React: "After this <input> element is created in the DOM, set inputRef.current to that DOM node."
  3. When the button is clicked, inputRef.current is the actual <input> DOM element, so calling .focus() works just like vanilla JavaScript.
  4. When the component unmounts, React sets inputRef.current back to null.

Measuring elements

jsx
function MeasuredBox() {
  const boxRef = React.useRef(null);
  const [dimensions, setDimensions] = React.useState({ width: 0, height: 0 });

  React.useEffect(() => {
    if (boxRef.current) {
      const rect = boxRef.current.getBoundingClientRect();
      setDimensions({ width: rect.width, height: rect.height });
    }
  }, []);

  return (
    <div>
      <div ref={boxRef} style={{ width: '200px', height: '100px', background: '#eee' }}>
        Measured box
      </div>
      <p>Width: {dimensions.width}px, Height: {dimensions.height}px</p>
    </div>
  );
}

Since the ref is null during the first render (the DOM element does not exist yet), we use useEffect to read the dimensions after the element has been painted.

useRef for Mutable Values

Beyond DOM access, useRef is useful for storing any mutable value that should persist between renders but should not trigger a re-render when changed.

Storing previous values

jsx
function Counter() {
  const [count, setCount] = React.useState(0);
  const prevCountRef = React.useRef(0);

  React.useEffect(() => {
    prevCountRef.current = count;
  });

  return (
    <p>
      Current: {count}, Previous: {prevCountRef.current}
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </p>
  );
}

Storing interval/timeout IDs

Refs are perfect for holding timer IDs that need to be cleared later:

jsx
function Stopwatch() {
  const [seconds, setSeconds] = React.useState(0);
  const intervalRef = React.useRef(null);

  function start() {
    if (intervalRef.current) return; // already running
    intervalRef.current = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);
  }

  function stop() {
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  }

  // Cleanup on unmount
  React.useEffect(() => {
    return () => clearInterval(intervalRef.current);
  }, []);

  return (
    <div>
      <p>{seconds}s</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  );
}

If we used useState for the interval ID, every start/stop would trigger an unnecessary re-render. Since we never need to display the interval ID, a ref is the right choice.

The rule of thumb

Use useStateUse useRef
Value is displayed in the UIValue is not displayed in the UI
Changing it should trigger a re-renderChanging it should not trigger a re-render
Examples: counter, form input, toggleExamples: timer ID, previous value, DOM node

Forwarding Refs and Imperative Handle

By default, function components do not expose their internal DOM nodes. If a parent wants to focus an input inside a child component, you need to forward the ref.

forwardRef

React.forwardRef creates a component that can receive a ref from its parent and forward it to a DOM element:

jsx
const FancyInput = React.forwardRef(function FancyInput(props, ref) {
  return (
    <input
      ref={ref}
      type="text"
      style={{ border: '2px solid blue', padding: '8px' }}
      {...props}
    />
  );
});

// Parent can now ref the inner <input>
function Form() {
  const inputRef = React.useRef(null);
  return (
    <div>
      <FancyInput ref={inputRef} placeholder="Fancy!" />
      <button onClick={() => inputRef.current.focus()}>Focus</button>
    </div>
  );
}

useImperativeHandle

Sometimes you want to expose only specific methods to the parent, rather than the entire DOM node. useImperativeHandle lets you customize the ref value:

jsx
const FancyInput = React.forwardRef(function FancyInput(props, ref) {
  const inputRef = React.useRef(null);

  React.useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current.focus();
    },
    clear() {
      inputRef.current.value = '';
    },
  }));

  return <input ref={inputRef} {...props} />;
});

// Parent sees only focus() and clear(), not the raw DOM node
function Form() {
  const inputRef = React.useRef(null);
  return (
    <div>
      <FancyInput ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>Focus</button>
      <button onClick={() => inputRef.current.clear()}>Clear</button>
    </div>
  );
}

This pattern follows the principle of least privilege — the parent gets only the API it needs, not direct DOM access.

Refs in Action

html
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
  function App() {
    const inputRef = React.useRef(null);
    const renderCount = React.useRef(0);
    const [text, setText] = React.useState("");

    renderCount.current += 1;

    return (
      <div>
        <h2>useRef Demo</h2>
        <input
          ref={inputRef}
          value={text}
          onChange={e => setText(e.target.value)}
          placeholder="Type something..."
        />
        <button onClick={() => inputRef.current.focus()}>
          Focus Input
        </button>
        <button onClick={() => { inputRef.current.value = ""; setText(""); }}>
          Clear
        </button>
        <p>You typed: {text}</p>
        <p>Render count: {renderCount.current}</p>
      </div>
    );
  }

  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>

What happens when you change the .current property of a ref created with useRef?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.