Stop Storing Everything in useState: Three Patterns Where useRef Gets It Right
Misclassifying non-UI values as state causes unnecessary re-renders and introduces stale-closure bugs that are hard to diagnose. Recognizing when a value is external to React's data flow — and reaching for useRef instead of useState — eliminates both problems at the source.
A counter that mutates state directly without a setter silently fails to update the UI, exposing a common React misunderstanding. The root issue is treating every stored value as reactive UI state. useRef provides a persistent, mutable container whose changes never trigger a re-render — a deliberate design, not a performance hack.
Three concrete patterns separate useRef from useState. Binding DOM nodes via the ref attribute avoids a pointless double render. Storing mutable values like timer IDs sidesteps stale closures that plague useState in event handlers. Holding external objects — Web Workers, WebSocket connections, AbortController instances — keeps infrastructure out of the render cycle entirely.
The decision tree is straightforward: if a value change should update the UI, reach for useState. If it should not, useRef is the answer. Recognizing a DOM node or a Worker as external to React's data flow eliminates an entire category of unnecessary renders and closure bugs.
The article frames useRef not as a performance optimization but as a semantic tool — its purpose is to hold things that are not UI state, and the avoided re-render is a consequence, not the goal.
The stale-closure argument for useRef over useState is the strongest practical reason given: a ref's object identity never changes, so any closure reading .current always sees the latest value, sidestepping an entire class of React bugs.
The three-layer breakdown for why Worker needs useRef (semantic, performance, correctness) is a useful framework for evaluating any value's proper home in a React component.