Latest Ref Pattern : Access the Latest Value Without Re-running Effects
Keep event handlers up to date using a ref instead of effect dependencies.
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.