React useRef Hook

Learn useRef fundamentals: DOM access, callback refs, useImperativeHandle, and React 19 ref-as-prop.

TL;DR

  1. 01useRef persists a .current value across renders without re-rendering.
  2. 02React 19 passes ref as a prop — forwardRef is deprecated.
  3. 03useImperativeHandle lets a child component expose a controlled API.

Tips

  1. 01Initialise useRef with the type of value you expect. Pass null for DOM refs and a real initial value (e.g. 0, false) for mutable value refs — this makes TypeScript inference easier.

Warnings

  1. 01A ref mutation inside render is allowed only for the renders counter pattern shown above where you don't use the value for display. Any ref read that feeds JSX output must happen in an effect.

useRef Basics

useRef creates a plain JavaScript object with a single .current property. React keeps the same object across every render — mutating .current never schedules a re-render.

  • Attach a ref to a DOM element to call browser methods on it.
    import { useRef } from 'react';
    
    function TextInput() {
      const inputRef = useRef(null);
    
      function handleClick() {
        inputRef.current.focus(); // direct DOM call
      }
    
      return (
        <>
          <input ref={inputRef} placeholder="Type here" />
          <button onClick={handleClick}>Focus input</button>
        </>
      );
    }
  • Store any mutable value — not just DOM nodes. Here a render counter persists without triggering re-renders.
    function RenderCounter() {
      const renders = useRef(0);
      renders.current += 1; // mutate during render is fine for refs
    
      return <p>Rendered {renders.current} times (no re-render loop)</p>;
    }
  • Key distinction: ref vs state.
    • ref.current changes — component does not re-render, UI stays the same.
    • setState changes — component re-renders, UI updates.

React 19: Refs as Props

Before React 19, passing a ref into a child component required wrapping it in forwardRef. React 19 removes that requirement — ref is now a plain prop, just like className or onClick.

  • Before (React 18 and earlier) — forwardRef wrapper required:
    // React 18: you had to wrap in forwardRef
    const Input = forwardRef(function Input(props, ref) {
      return <input ref={ref} {...props} />;
    });
    
    function Form() {
      const inputRef = useRef(null);
      return <Input ref={inputRef} placeholder="Email" />;
    }
  • After (React 19) — ref arrives as a plain prop:
    // React 19: ref is just a prop
    function Input({ ref, ...props }) {
      return <input ref={ref} {...props} />;
    }
    
    function Form() {
      const inputRef = useRef(null);
      return <Input ref={inputRef} placeholder="Email" />;
    }
  • Class components still use createRef and are unaffected by this change.
  • forwardRef still works in React 19 for library backward compatibility but shows a deprecation warning in dev mode.

Callback Refs

Instead of passing a ref object to the ref prop, you can pass a function. React calls it with the DOM node on mount and null on unmount. This is useful when you need to act on the node the instant it appears — before any effect would fire.

  • Measure an element's size the moment it mounts:
    function MeasuredBox() {
      const [height, setHeight] = useState(null);
    
      const measuredRef = useCallback((node) => {
        if (node !== null) {
          setHeight(node.getBoundingClientRect().height);
        }
      }, []); // stable function — no deps needed
    
      return (
        <>
          <div ref={measuredRef} style={{ padding: 20 }}>
            Resize me
          </div>
          {height !== null && <p>Height: {height}px</p>}
        </>
      );
    }
  • The callback fires again when the element re-mounts (e.g. after conditional rendering), giving you an automatic measurement update — a regular ref object would not trigger this.
  • Wrap the callback in useCallback with a stable dependency array to avoid React re-running the ref function on every render.
  • Clean-up pattern — run code on both attach and detach:
    const logRef = useCallback((node) => {
      if (node) {
        console.log('mounted:', node.tagName);
      } else {
        console.log('unmounted');
      }
    }, []);
    
    return <div ref={logRef}>Watch the console</div>;

useImperativeHandle

useImperativeHandle lets a child component decide exactly which methods a parent can call through a ref. Instead of exposing the raw DOM node, the child publishes a controlled object.

  • Expose focus and clear without leaking the internal input ref:
    // React 19 style — ref as a prop, no forwardRef needed
    function FancyInput({ ref }) {
      const inputRef = useRef(null);
    
      useImperativeHandle(ref, () => ({
        focus() {
          inputRef.current.focus();
        },
        clear() {
          inputRef.current.value = '';
        },
      }));
    
      return <input ref={inputRef} className="fancy" />;
    }
    
    function Form() {
      const fancyRef = useRef(null);
    
      return (
        <>
          <FancyInput ref={fancyRef} />
          <button onClick={() => fancyRef.current.focus()}>Focus</button>
          <button onClick={() => fancyRef.current.clear()}>Clear</button>
        </>
      );
    }
  • Pass a dependency array as the third argument to control when the handle object is recreated:
    useImperativeHandle(ref, () => ({ getValue: () => value }), [value]);
  • Restrict the exposed API intentionally — if you don't expose scrollIntoView, the parent simply cannot call it.
    useImperativeHandle(ref, () => ({
      focus: () => inputRef.current.focus(),
      // scrollIntoView intentionally not exposed
    }));

Best Practices

  • Prefer state over refs for UI values. If a value change should update the screen, use useState — not a ref.
    // Wrong: ref won't update the UI
    const count = useRef(0);
    const increment = () => { count.current++; }; // no re-render
    
    // Right: state drives the UI
    const [count, setCount] = useState(0);
    const increment = () => setCount(c => c + 1);
  • Never read refs during rendering. The DOM node is null until after mount. Reading it during render returns null at best and throws at worst.
    // Wrong: ref is null here
    function Bad() {
      const ref = useRef(null);
      const width = ref.current?.offsetWidth; // always undefined on first render
      return <div ref={ref}>{width}px</div>;
    }
    
    // Right: read in effect or event handler
    function Good() {
      const ref = useRef(null);
      const [width, setWidth] = useState(0);
      useEffect(() => { setWidth(ref.current.offsetWidth); }, []);
      return <div ref={ref}>{width}px</div>;
    }
  • React 19: drop forwardRef in new components. Accept ref as a plain prop — it's simpler and the JSX tools generate better TypeScript types automatically.
  • Use callback refs for dynamic elements. When an element may mount and unmount (e.g. behind a conditional), a callback ref ensures you always run your setup code at the right time.
  • Don't store derived data in refs. Compute it during render instead — refs that shadow state become stale and are hard to debug.
    // Wrong: keeping a ref in sync with state manually
    const doubleRef = useRef(count * 2);
    
    // Right: derive during render
    const double = count * 2;

FAQ