Technology · React

React Ref Patterns

Use useRef for DOM access, timers, and storing mutable values.

TL;DR
  1. 01Use useRef to access DOM nodes directly when needed.
  2. 02Store mutable values that don't trigger re-renders.
  3. 03Use forwardRef to pass refs to child components.

DOM Access with useRef

  • Create a ref to access DOM elements 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 methods.
    function VideoPlayer() {
      const videoRef = useRef(null);
      
      function handlePlay() {
        videoRef.current.play();
      }
      
      function handlePause() {
        videoRef.current.pause();
      }
      
      return (
        <>
          <video ref={videoRef} />
          <button onClick={handlePlay}>Play</button>
          <button onClick={handlePause}>Pause</button>
        </>
      );
    }

Managing Timers and Intervals

  • Store timer IDs in refs to clear them 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 timers on unmount.
    useEffect(() => {
      return () => {
        if (intervalRef.current) {
          clearInterval(intervalRef.current);
        }
      };
    }, []);

Storing Mutable Values

  • Use refs to store values that don't trigger re-renders.
    function Counter() {
      const countRef = useRef(0);
      
      function increment() {
        countRef.current++;
        console.log(`Count: ${countRef.current}`);
      }
      
      return (
        <>
          <p>Ref count: {countRef.current}</p>
          <button onClick={increment}>Increment</button>
        </>
      );
    }
  • Track previous state with refs.
    function Counter() {
      const [count, setCount] = useState(0);
      const prevCountRef = useRef();
      
      useEffect(() => {
        prevCountRef.current = count;
      }, [count]);
      
      return <p>Now: {count}, Before: {prevCountRef.current}</p>;
    }
  • Store flags and metadata without re-renders.
    const isFirstRender = useRef(true);
    
    useEffect(() => {
      if (isFirstRender.current) {
        isFirstRender.current = false;
        // run only on mount
      }
    }, []);

forwardRef for Child Components

  • Pass refs to child components with forwardRef.
    const TextInput = forwardRef((props, ref) => (
      <input ref={ref} />
    ));
    
    function Parent() {
      const inputRef = useRef(null);
      
      return (
        <>
          <TextInput ref={inputRef} />
          <button onClick={() => inputRef.current.focus()}>
            Focus
          </button>
        </>
      );
    }
  • Expose custom methods with useImperativeHandle.
    const TextInput = forwardRef((props, ref) => {
      const inputRef = useRef(null);
      
      useImperativeHandle(ref, () => ({
        focus: () => inputRef.current.focus(),
        clear: () => { inputRef.current.value = ""; }
      }));
      
      return <input ref={inputRef} />;
    });

Common Patterns

  • Detect clicks outside a ref element.
    function Dropdown() {
      const dropdownRef = useRef(null);
      const [open, setOpen] = useState(false);
      
      useEffect(() => {
        function handleClickOutside(e) {
          if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
            setOpen(false);
          }
        }
        
        document.addEventListener("mousedown", handleClickOutside);
        return () => document.removeEventListener("mousedown", handleClickOutside);
      }, []);
      
      return <div ref={dropdownRef}>{/* dropdown content */}</div>;
    }
  • Measure element size with refs.
    function SizeDisplay() {
      const divRef = useRef(null);
      const [size, setSize] = useState(null);
      
      function measureSize() {
        const rect = divRef.current.getBoundingClientRect();
        setSize({ width: rect.width, height: rect.height });
      }
      
      return (
        <div ref={divRef} onClick={measureSize}>
          Size: {size && `${size.width}x${size.height}`}
        </div>
      );
    }

Tip: Use refs sparingly — prefer state and props for most cases. Refs are for when you need direct DOM access or to store mutable values.

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

React Props and ChildrenReact State Management