Useful Cheatsheetsusefulcheatsheets.com
React Ref Patterns
Chapter 16 · Page 112
Intermediate

React Ref Patterns

Real-world React ref patterns: click-outside detection, ref arrays, previous value tracking, ResizeObserver, and TypeScript typing.

TL;DR

  1. 01Use ref.current.contains() to detect clicks outside a component.
  2. 02Track a previous value by assigning to a ref inside useEffect.
  3. 03Combine useRef with ResizeObserver for accurate element measurements.

Tips

  1. 01Wrap the callback prop in useCallback in the parent to keep the dependency array stable and avoid re-registering the document listener on every render.

Warnings

  1. 01On the very first render, usePrevious returns undefined because no previous value exists. Guard with prevValue !== undefined before using it in comparisons.
Notes
Useful Cheatsheetsusefulcheatsheets.com
React Ref Patterns
Chapter 16 · Page 113
Intermediate

React Ref Patterns

(continued)

Click-Outside Detection

The click-outside pattern is essential for dropdowns, modals, and tooltips. Attach a ref to the container, listen for mousedown on document, and call contains() to decide whether to close.

  • Full reusable hook:
    import { useRef, useEffect } from 'react';
    
    function useClickOutside(callback) {
      const ref = useRef(null);
    
      useEffect(() => {
        function handleMouseDown(e) {
          if (ref.current && !ref.current.contains(e.target)) {
            callback();
          }
        }
        document.addEventListener('mousedown', handleMouseDown);
        return () => document.removeEventListener('mousedown', handleMouseDown);
      }, [callback]);
    
      return ref;
    }
    
    function Dropdown() {
      const [open, setOpen] = useState(false);
      const dropdownRef = useClickOutside(() => setOpen(false));
    
      return (
        <div ref={dropdownRef}>
          <button onClick={() => setOpen(o => !o)}>Toggle</button>
          {open && <ul><li>Item 1</li><li>Item 2</li></ul>}
        </div>
      );
    }
  • Use mousedown rather than click so the close action fires before any click handler on the newly focused element. This prevents flickering when clicking between two dropdowns.
  • Guard with ref.current && before calling contains — the ref may be null if the component unmounted between renders.
Notes
Useful Cheatsheetsusefulcheatsheets.com
React Ref Patterns
Chapter 16 · Page 114
Intermediate

React Ref Patterns

(continued)

Ref Arrays for Dynamic Lists

When you render a list with map and need a ref to each item, you can't call useRef in a loop (that violates the Rules of Hooks). Instead, store an array inside a single ref and populate it with callback refs.

  • Hold all item refs in one ref object and assign each slot via a callback ref:
    import { useRef } from 'react';
    
    function VirtualList({ items }) {
      const itemRefs = useRef([]);
    
      function scrollToItem(index) {
        itemRefs.current[index]?.scrollIntoView({ behavior: 'smooth' });
      }
    
      return (
        <>
          <button onClick={() => scrollToItem(5)}>Jump to #5</button>
          <ul>
            {items.map((item, i) => (
              <li
                key={item.id}
                ref={node => { itemRefs.current[i] = node; }}
              >
                {item.label}
              </li>
            ))}
          </ul>
        </>
      );
    }
  • The inline callback node => { itemRefs.current[i] = node; } runs each render, keeping the array in sync when items are added or removed. When a node unmounts React calls the callback with null, so you may want to filter: itemRefs.current = itemRefs.current.filter(Boolean) before use.
  • This pattern also works with Map keyed by item ID for sparse or keyed access:
    const refMap = useRef(new Map());
    // ...
    ref={node => {
      if (node) refMap.current.set(item.id, node);
      else refMap.current.delete(item.id);
    }}
Notes
Useful Cheatsheetsusefulcheatsheets.com
React Ref Patterns
Chapter 16 · Page 115
Intermediate

React Ref Patterns

(continued)

Previous Value Tracking

React has no built-in way to read a previous prop or state value, but a ref updated inside useEffect gives you exactly that — the value from the render that just committed.

  • Generic usePrevious hook:
    import { useRef, useEffect } from 'react';
    
    function usePrevious(value) {
      const ref = useRef(undefined);
      useEffect(() => {
        ref.current = value; // runs AFTER render, so ref holds previous value during render
      }, [value]);
      return ref.current;
    }
    
    function PriceDisplay({ price }) {
      const prevPrice = usePrevious(price);
      const direction = price > prevPrice ? '↑' : price < prevPrice ? '↓' : '';
    
      return (
        <p>
          {price} {direction} (was {prevPrice ?? 'N/A'})
        </p>
      );
    }
  • How the timing works: during render, ref.current still holds the previous value because the effect hasn't fired yet. After React commits the DOM, the effect updates ref.current to the new value, ready for the next render.
  • Use this to skip an effect on the initial render by comparing current and previous values, or to animate differences between frames.
Notes
Useful Cheatsheetsusefulcheatsheets.com
React Ref Patterns
Chapter 16 · Page 116
Intermediate

React Ref Patterns

(continued)

Measuring with ResizeObserver

ResizeObserver fires whenever a watched element's content box changes size. Combining it with useRef gives you live width and height measurements without polling or window resize events.

  • Full hook that returns live dimensions:
    import { useRef, useState, useEffect } from 'react';
    
    function useElementSize() {
      const ref = useRef(null);
      const [size, setSize] = useState({ width: 0, height: 0 });
    
      useEffect(() => {
        const node = ref.current;
        if (!node) return;
    
        const observer = new ResizeObserver(([entry]) => {
          const { width, height } = entry.contentRect;
          setSize({ width: Math.round(width), height: Math.round(height) });
        });
    
        observer.observe(node);
        return () => observer.disconnect(); // always clean up
      }, []);
    
      return [ref, size];
    }
    
    function ResponsiveCard() {
      const [cardRef, { width, height }] = useElementSize();
    
      return (
        <div ref={cardRef} style={{ resize: 'both', overflow: 'auto', padding: 16 }}>
          <p>{width} × {height}px — drag the corner to resize</p>
        </div>
      );
    }
  • ResizeObserver is available in all modern browsers (Chrome 64+, Firefox 69+, Safari 13.1+). For older environments, polyfill with resize-observer-polyfill.
  • The observer fires once immediately after observe() is called, so size is populated on mount without an extra effect or state initialization.
Notes
Useful Cheatsheetsusefulcheatsheets.com
React Ref Patterns
Chapter 16 · Page 117
Intermediate

React Ref Patterns

(continued)

TypeScript: Typing Refs

TypeScript has two ref types. Choosing the right one avoids unsafe non-null assertions and keeps autocompletion accurate.

TypecurrentCreated byUse case
RefObject<T>T | null (readonly)useRef<T>(null)DOM nodes React controls
MutableRefObject<T>T (writable)useRef<T>(initialValue)Timer IDs, counters, previous values
  • DOM element ref — always start with null:
    const inputRef = useRef<HTMLInputElement>(null);
    // inputRef is RefObject<HTMLInputElement>
    // Safe access with optional chaining:
    inputRef.current?.focus();
  • Mutable value ref — supply a real initial value so TypeScript infers MutableRefObject:
    const timerId = useRef<ReturnType<typeof setTimeout> | null>(null);
    // Assign later:
    timerId.current = setTimeout(fn, 500);
    clearTimeout(timerId.current!);
  • Typing a component that accepts a ref (React 19 style):
    import { useRef, useImperativeHandle } from 'react';
    
    type InputHandle = {
      focus: () => void;
      clear: () => void;
    };
    
    type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
      ref?: React.Ref<InputHandle>;
    };
    
    function FancyInput({ ref, ...props }: InputProps) {
      const innerRef = useRef<HTMLInputElement>(null);
    
      useImperativeHandle(ref, () => ({
        focus: () => innerRef.current?.focus(),
        clear: () => { if (innerRef.current) innerRef.current.value = ''; },
      }));
    
      return <input ref={innerRef} {...props} />;
    }
    
    // Parent:
    const formRef = useRef<InputHandle>(null);
    <FancyInput ref={formRef} placeholder="Email" />
  • Avoid using as React.MutableRefObject<T> to force-cast DOM refs — this bypasses null safety. Instead, use optional chaining or an early-return null check.
Notes
Useful Cheatsheetsusefulcheatsheets.com
React Ref Patterns
Chapter 16 · Page 118
Intermediate

React Ref Patterns

(FAQ)

FAQ

Attach a ref to the dropdown container, then add a mousedown listener on document inside a useEffect. In the handler, call ref.current.contains(e.target) — if it returns false, the click was outside and you can close the dropdown. Remove the listener in the cleanup function returned from useEffect.

Initialise a ref with useRef([]) to hold the array of DOM nodes, then pass each item a callback ref function that assigns node into the array at the correct index: ref={node => { listRef.current[i] = node; }}. This approach works even when the list length changes between renders.

Create a ref with useRef(), then run useEffect(() => { prevRef.current = value; }, [value]) after each render. Because effects run after the render is committed, prevRef.current always holds the value from the previous render cycle when you read it during the current render.

Use ResizeObserver when you need continuous size updates as the element grows or shrinks — for example, when the user resizes the window or when dynamic content changes the element's dimensions. getBoundingClientRect is a one-shot read and won't notify you of future changes. ResizeObserver fires a callback each time the observed element's size changes.

RefObject has a readonly current property typed as T | null — it models a DOM ref that React controls. MutableRefObject has a mutable current typed as T — it models a ref you update yourself, like a timer ID or previous value store. useRef(null) returns RefObject when given a type argument like useRef(null), while useRef(initialValue) with a non-null value returns MutableRefObject.