Skip to content
Daily Dev Tip

This tip lives in your new tab — for free.

Install for Chrome →
REACT

The stale closure, your debounced callback saves old state

js
function useDebouncedCallback(callback, delay) {
  const latest = useRef(callback);
  useEffect(() => {
    latest.current = callback; // refresh every render
  });
  return useMemo(() => {
    let t;
    return (...args) => {
      clearTimeout(t);
      t = setTimeout(() => latest.current(...args), delay); // fresh closure
    };
  }, [delay]);
}
const saveDraft = useDebouncedCallback(() => api.save(form), 800);
// form is current when the timer fires, not the mount-time value

Stabilizing a debounce with `useMemo(() => debounce(fn, 300), [])` fixes one bug and creates a worse one: the callback is frozen at mount, so it captures the initial state forever.

Classic symptom → debounced autosave that saves an empty form.

The fix: the latest-ref pattern. - The debounced shell stays stable, so timers survive re-renders - It calls through a ref that always points at the newest closure

react19debounceperformancejs