Caveat: using useRef with setTimeout
useReflets you hold a mutable value or DOM reference across renders without causing re-renders.- When to use:
- Accessing or imperatively manipulating DOM nodes
- Storing mutable “instance” values (e.g. timers, latest state)
- Fixing stale-closure bugs in asynchronous callbacks
What is useRef?
In React, every render “re-runs” your component function, so local variables get reset. useRef returns a stable object:
const refContainer = useRef<T>(initialValue);
refContainer.currentpersists across renders.- Updating
.currentdoes not trigger a re-render.
You can think of it like a “box” you keep beside your component—whatever you put in that box stays there until you overwrite it, but React won’t inspect it to decide whether to draw again.
Core Use Cases
| Scenario | Solution | Why useRef? |
|---|---|---|
| DOM access | const elRef = useRef(null) | Imperatively focus, measure, or scroll |
| Storing timers or intervals | const timerRef = useRef<NodeJS.Timeout>() | Clear timer on unmount without re-renders |
| Latest state or props in async code | const latest = useRef(value) | Avoid stale closures in callbacks |
| Third-party libraries | const chartRef = useRef() | Pass DOM node to non-React code |
💡 Note: If you want React to re-render on change, use
useStateinstead.useRefis for mutable, non-render-triggering data.
Avoiding Stale Closures in Async Callbacks
Imagine you schedule a setTimeout that reads a piece of state:
function processNext() {
if (connectionStatus === "disconnected") {
// …
}
}
setTimeout(processNext, 500);
Because JavaScript closes over the value at definition time, processNext might see an outdated connectionStatus.
The Stale-Closure Sequence
Fix with useRef
-
Store the latest value in a ref:
const connectionRef = useRef(connectionStatus);
useEffect(() => {
connectionRef.current = connectionStatus;
}, [connectionStatus]); -
Read from
ref.currentinside your async callback:function processNext() {
if (connectionRef.current === "disconnected") {
setCurrentStatus(null);
}
// …
}
Now, regardless of when processNext runs, it always reads the up-to-date status.
When Not to Reach for useRef
- State you render (UI logic)—use
useState. - Derived values—compute them directly or memoise with
useMemo.
useRef shines when you need an escape hatch: imperative DOM work, timers, or stunning stale-closure demons.
Further Reading
- React Docs:
useRef - Overreacted: “A Complete Guide to useEffect” (for context on hooks and timing)