useLatest Ref: The Anti-Stale-Closure Pattern

The five-line hook I reach for whenever an effect, a setTimeout, or an external subscription needs to call back into the latest value of a prop or state without re-binding.

JavaScript
Frontend
2 snippets
react
hooks
code-template
utility
petrawilson

By @petrawilson

December 24, 2025

·

Updated August 11, 2026

852 views

20

4.4 (8)

// useLatest: a one-line hook that holds a ref pointing at the most recent value.
// Solves the stale-closure bug where setTimeout / subscriptions see a prop from
// the render in which they were registered, not the latest one.
const { useRef } = (typeof React !== 'undefined' ? React : {
    useRef: (init) => ({ current: init }),
});

function useLatest(value) {
    const ref = useRef(value);
    ref.current = value;  // assign on every render, no useEffect needed
    return ref;
}

// Demo: simulate three renders with different `count` props, holding the same
// ref across renders the way React's reconciler does. The stored callback
// captures `count` directly OR reads from the ref, and we compare what each sees.
const countRef = { current: undefined };  // persistent across simulated renders
let storedStale = null;
let storedFresh = null;

function simulateRender(count) {
    // Hook body, equivalent to: const countRef = useLatest(count);
    countRef.current = count;
    // Register handlers only on the first render, then never again.
    if (!storedStale) storedStale = () => console.log('stale closure sees count =', count);
    if (!storedFresh) storedFresh = () => console.log('via useLatest sees count =', countRef.current);
}

simulateRender(0);
simulateRender(1);
simulateRender(2);

storedStale();  // sees 0 (the render in which it was bound)
storedFresh();  // sees 2 (the latest render's value)

useLatest is the smallest custom hook I keep around: three lines of body, one assignment per render. The bug it solves is the most common cause of "why is my React app stale": a setTimeout or websocket handler is registered once, and the function it calls captured count from the render that registered it, not the current one. Running the assignment outside useEffect is intentional: effects fire after commit, but the render itself is what the next callback should see. The shim above does not re-render across calls, so accordion 1 fakes two renders to print the contrast: stale closure sees 0, useLatest sees the most recent value.