Latest Ref Pattern : Access the Latest Value Without Re-running Effects
Keep event handlers up to date using a ref instead of effect dependencies.
Introduction
The latest ref pattern stores the latest version of a value (often a function) in a ref so you can use it inside long‑lived handlers without re‑subscribing effects.
Why this matters
Event listeners and subscriptions often need access to the latest callback. Adding functions to effect deps causes the effect to re‑subscribe on every render. A ref keeps the latest function without re‑running the effect.
The problem
If you capture a callback in an effect without keeping it in sync, the listener uses a stale closure. If you add the callback to the dependencies, the effect re-subscribes every render.
Inefficient approach
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;
The solution
Store the latest onClick in a ref, update it every render, and use it inside a once‑registered listener.
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;
Step-by-step
- Create
const onClickRef = useRef(onClick). - In a no-deps effect, update
onClickRef.current = onClickevery render. - Register the DOM listener once; call
onClickRef.current()inside. - Clean up the listener on unmount.
Tips
- Initialize the ref with the first function value for clarity.
- Keep the “update ref” effect free of deps to run every render.
- This pattern generalizes to any subscription that needs fresh callbacks.
Knowledge base
Problem snippet:
document.addEventListener("click", () => onClick());
Solution snippet:
const onClickRef = useRef(onClick);
onClickRef.current = onClick;
document.addEventListener("click", () => onClickRef.current());