Technology · React

React useRef Hook

Use useRef to access DOM elements directly and store mutable values.

TL;DR
  1. 01Use useRef to get direct access to DOM nodes.
  2. 02Store mutable values that don't trigger re-renders.
  3. 03useImperativeHandle exposes methods to parent components.

Accessing DOM Elements

  • Create a ref to access DOM nodes directly.
    function TextInput() {
      const inputRef = useRef(null);
      
      function handleClick() {
        inputRef.current.focus();
      }
      
      return (
        <>
          <input ref={inputRef} />
          <button onClick={handleClick}>Focus input</button>
        </>
      );
    }
  • Access element properties and call methods.
    function VideoPlayer() {
      const videoRef = useRef(null);
      
      return (
        <>
          <video ref={videoRef} />
          <button onClick={() => videoRef.current.play()}>
            Play
          </button>
        </>
      );
    }
  • Refs persist across re-renders without triggering them.
  • Read scroll position or dimensions from a DOM element via ref.
    function ScrollTracker() {
      const boxRef = useRef(null);
      function logSize() {
        const { width, height } = boxRef.current.getBoundingClientRect();
        console.log(width, height);
      }
      return <div ref={boxRef} onClick={logSize}>Click me</div>;
    }
  • Attach refs to any host element: input, div, canvas, video, and more.
    const canvasRef = useRef(null);
    useEffect(() => {
      const ctx = canvasRef.current.getContext("2d");
      ctx.fillRect(0, 0, 100, 100);
    }, []);
    return <canvas ref={canvasRef} width={200} height={200} />;

Storing Mutable Values

  • Use refs to store values that don't cause re-renders.
    function Counter() {
      const countRef = useRef(0);
      
      function increment() {
        countRef.current++;
        console.log(`Count: ${countRef.current}`);
      }
      
      return (
        <button onClick={increment}>
          Increment (see console)
        </button>
      );
    }
  • Track previous state or props with refs.
    function Counter({ value }) {
      const prevValueRef = useRef();
      
      useEffect(() => {
        prevValueRef.current = value;
      }, [value]);
      
      return <p>Now: {value}, Before: {prevValueRef.current}</p>;
    }
  • Refs don't trigger re-renders when updated.
  • Store a ref to the latest callback to avoid stale closure issues in effects.
    function useEvent(handler) {
      const handlerRef = useRef(handler);
      useEffect(() => { handlerRef.current = handler; });
      return useCallback((...args) => handlerRef.current(...args), []);
    }
  • Use a ref to track whether the component is still mounted before updating state.
    function useSafeState(initial) {
      const isMounted = useRef(true);
      const [state, setState] = useState(initial);
      useEffect(() => () => { isMounted.current = false; }, []);
      const safeSet = useCallback(v => { if (isMounted.current) setState(v); }, []);
      return [state, safeSet];
    }

useImperativeHandle

  • Expose custom methods to parent components.
    const TextInput = forwardRef((props, ref) => {
      const inputRef = useRef(null);
      
      useImperativeHandle(ref, () => ({
        focus: () => inputRef.current.focus(),
        clear: () => { inputRef.current.value = ""; }
      }));
      
      return <input ref={inputRef} />;
    });
    
    function Parent() {
      const inputRef = useRef(null);
      
      return (
        <>
          <TextInput ref={inputRef} />
          <button onClick={() => inputRef.current.focus()}>
            Focus
          </button>
        </>
      );
    }
  • Only expose methods you want parents to call.
  • forwardRef is required to receive a ref prop in a function component.
    const Input = forwardRef((props, ref) => <input ref={ref} {...props} />);
    
    function Form() {
      const ref = useRef(null);
      return <Input ref={ref} placeholder="Name" />;
    }
  • Restrict exposed API to prevent parents from misusing the component.
    useImperativeHandle(ref, () => ({
      focus: () => inputRef.current.focus(),
      // scrollIntoView NOT exposed — parent shouldn't control that
    }));
  • Pass a second argument to useImperativeHandle to control when it re-runs.
    useImperativeHandle(ref, () => ({ getValue: () => value }), [value]);

Managing Timers and Intervals

  • Store timer IDs in refs to clean up later.
    function Stopwatch() {
      const intervalRef = useRef(null);
      const [time, setTime] = useState(0);
      
      function start() {
        intervalRef.current = setInterval(() => {
          setTime(t => t + 1);
        }, 1000);
      }
      
      function stop() {
        clearInterval(intervalRef.current);
      }
      
      return (
        <>
          <p>Time: {time}s</p>
          <button onClick={start}>Start</button>
          <button onClick={stop}>Stop</button>
        </>
      );
    }
  • Clean up intervals on unmount with useEffect.
    useEffect(() => {
      return () => {
        if (intervalRef.current) {
          clearInterval(intervalRef.current);
        }
      };
    }, []);
  • Store a debounce timeout ref to cancel the previous call on each keystroke.
    const timeoutRef = useRef(null);
    function handleChange(e) {
      clearTimeout(timeoutRef.current);
      timeoutRef.current = setTimeout(() => search(e.target.value), 300);
    }
  • Use requestAnimationFrame refs for smooth animations without state updates.
    const rafRef = useRef(null);
    function startAnimation() {
      function step() { /* update canvas */ rafRef.current = requestAnimationFrame(step); }
      rafRef.current = requestAnimationFrame(step);
    }
    useEffect(() => () => cancelAnimationFrame(rafRef.current), []);

Best Practices

  • Use refs sparingly — prefer state and props first.
    // Bad: using ref when state would work
    const countRef = useRef(0);
    countRef.current++;
    
    // Good: use state for rendering
    const [count, setCount] = useState(0);
  • Never read or write refs during rendering.
    // Bad: reading ref during render
    function Component() {
      const ref = useRef(null);
      console.log(ref.current); // undefined during render
      return <div ref={ref}>Content</div>;
    }
    
    // Good: read in effect
    useEffect(() => {
      console.log(ref.current); // now it has a value
    }, []);
  • Initialize refs with initial values if needed.
    const ref = useRef(initialValue);
  • Type refs correctly in TypeScript by specifying the element type.
    const inputRef = useRef<HTMLInputElement>(null);
    inputRef.current?.focus(); // optional chain because it may be null
  • Avoid storing derived state in refs — compute it during render instead.
    // Bad: keeping a ref in sync with state manually
    const doubleRef = useRef(count * 2);
    // Good: just derive during render
    const double = count * 2;

Tip: Use refs for DOM access and imperative operations, but prefer state and props for most state management needs.

Warning: Changing ref.current doesn't trigger a re-render — if you need UI updates, use state instead.

React useContext Advanced Patterns