跪拜 Guibai
← All articles
React.js · Frontend Framework · Full Stack

Stop Storing Everything in useState: Three Patterns Where useRef Gets It Right

By 烬羽 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

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.

Summary

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.

Takeaways
useRef persists a value across renders and allows direct mutation via .current, but never triggers a re-render.
Storing a DOM node in useState via a ref callback causes a double render; useRef + the ref attribute avoids this entirely.
External objects like Web Workers, timer IDs, WebSocket connections, and AbortController instances belong in useRef, not useState.
useState captures a snapshot per render, so event handlers from an earlier render can hold stale values; useRef always returns the latest .current.
A Worker stored in useState can be null inside a stale closure even after setWorker has updated it, causing runtime errors.
The decision rule: if a value change should update the UI, use useState; otherwise, useRef.
Conclusions

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.

Concepts & terms
useRef
A React hook that returns a mutable object with a .current property. The object persists for the component's full lifetime, and mutating .current does not trigger a re-render.
Stale closure
In React, each render captures its own props, state, and handlers. A function created during an earlier render may hold references to values that have since been updated, leading to bugs when it reads outdated data.
Web Worker
A browser API that runs scripts in a background thread, separate from the main UI thread, preventing heavy computation from freezing the page.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗