A stable latest-callback ref to kill effect dependency churn
Published June 22, 2027
import { useRef, useEffect, useCallback } from "react";
export const useLatestCallback = <Args extends unknown[], R>(callback: (...args: Args) => R) => {
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
return useCallback((...args: Args) => callbackRef.current(...args), []);
};What
useLatestCallback wraps a callback in a ref that's always kept up to date, and returns a stable function identity that reads from that ref — so effects can call "whatever the latest callback is" without listing the callback itself as a dependency.
Why it matters
A callback prop (onChange, onMessage) is often a fresh function every render — an inline arrow, or one that closes over other props or state. Putting it directly in an effect's dependency array is correct per the rules of hooks, but it means the effect tears down and re-runs every time the parent re-renders with a new callback reference, even though the effect's actual job (subscribing to a WebSocket, setting an interval) hasn't changed at all. The usual bad fix is omitting the dependency and disabling the lint rule, which quietly captures a stale closure instead. This hook gives you a function whose identity never changes, while its behavior always matches the most recent callback passed in.
How it works
callbackRefstarts pointing at the firstcallbackit's given.- The effect keeps
callbackRef.currentin sync on every render wherecallbackchanged — this effect re-running is cheap and harmless, since all it does is update a ref. - The function returned by
useCallbackhas an empty dependency array, so its identity is stable for the lifetime of the component; calling it always delegates to whatevercallbackRef.currentcurrently is, not whatever it was when the function was created. - Because refs are mutable containers React doesn't consider "reactive," reading
callbackRef.currentinside the returned function doesn't need to appear in that function's own dependency list.
Gotchas
- The returned function runs the latest callback, not the one active when some earlier async operation started — if you need the callback as it existed at a specific point in time (not "right now"), capture it explicitly instead of going through this hook.
- This pattern is for effects and event subscriptions where you want stable identity; it's not a substitute for
useCallbackwhen a callback is passed to a memoized child that should actually re-render on a real prop change. - The one-render lag between
callbackchanging and the ref updating is invisible in practice (the ref updates before any other effect that depends on it could run), but it's worth knowing the update happens in an effect, not synchronously during render.
Related
useEffectEvent— React's own in-progress answer to this exact problem, currently shipped only under theexperimental_useEffectEventexport. Unlike this hook, it deliberately has no stable identity and can only be called from an Effect in the same component. Worth switching to once it stabilizes; this ref-based version is the portable way to get equivalent behavior today.useCallback— memoizes a callback's identity based on its dependencies; this hook sidesteps dependencies entirely by always reading the freshest closure through a ref.