Back to all notes

Latest Ref Pattern : Access the Latest Value Without Re-running Effects

Keep event handlers up to date using a ref instead of effect dependencies.

2 min read

A document-level click listener needs the current onClick, and the parent passes a new function every render. Keep the callback in the effect’s deps and the listener re-subscribes constantly. Drop it and the listener holds a stale closure. You get to pick which failure you want.

Stale Closure or Constant Re-Subscription

import { useEffect } from "react";

function ClickHandler({ onClick }) {
  useEffect(() => {
    const handleClick = () => onClick(); // stale if onClick changes
    document.addEventListener("click", handleClick);
    return () => document.removeEventListener("click", handleClick);
  }, [onClick]); // avoids staleness but re-subscribes every render

  return <p>Click anywhere on the page</p>;
}

function App() {
  const handleClick = () => {
    console.log("Clicked!");
  };

  return (
    <div>
      <h1>Latest Ref Pattern</h1>
      <ClickHandler onClick={handleClick} />
    </div>
  );
}

export default App;

This version keeps onClick in the deps, so it stays fresh but tears down and re-adds the listener on every render. Remove the dep and it subscribes once while calling a stale onClick forever.

The Ref That Always Has the Latest Callback

The way out is a ref that gets refreshed every render, so the listener can register once and still call through to whatever is current.

import { useRef, useEffect } from "react";

function ClickHandler({ onClick }) {
  const onClickRef = useRef(onClick);

  useEffect(() => {
    onClickRef.current = onClick;
  });

  useEffect(() => {
    const handleClick = () => {
      onClickRef.current();
    };
    document.addEventListener("click", handleClick);
    return () => document.removeEventListener("click", handleClick);
  }, []);

  return <p>Click anywhere on the page</p>;
}

function App() {
  const handleClick = () => {
    console.log("Clicked!");
  };

  return (
    <div>
      <h1>Latest Ref Pattern</h1>
      <ClickHandler onClick={handleClick} />
    </div>
  );
}

export default App;

useRef(onClick) seeds the ref with the first function. A dependency-free effect runs after every render and overwrites onClickRef.current with the newest prop. The listener effect takes [], subscribes once, and its handler calls onClickRef.current(). Cleanup on unmount is unchanged.

Beyond Click Listeners

Initialize the ref with the first function value, and keep the update effect dep-free so it runs every render. The same shape fits any long-lived subscription that needs a fresh callback. Click handlers are just the example here.