You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/react-guidelines/state.md
+71Lines changed: 71 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -220,6 +220,74 @@ export function useCurrentViewConfig() {
220
220
}
221
221
```
222
222
223
+
## Render Purity and Local State
224
+
225
+
### Don't read external state during render
226
+
227
+
React components [must be idempotent](https://react.dev/reference/rules/components-and-hooks-must-be-pure) - they should always return the same output with respect to their inputs. Reading from external sources like `localStorage`, `sessionStorage`, or browser APIs during render violates this because they are mutable sources outside React's control (same category as `Date.now()` or `Math.random()`). Beyond purity, these reads might create new references on every call (if you create new objects like parsing JSON or creating a new `Date`), which cause stale-value bugs and can contribute to unnecessary re-renders in children.
228
+
229
+
```typescript
230
+
// Bad: reads localStorage on every render, creates new arrays each time
231
+
function useActivityFilters(urlParams:URLSearchParams) {
If your project has a storage-aware hook like `useLocalStorageState`, prefer it over rolling the pattern by hand - they typically wrap the initial read in `useState(() => ...)`, sync writes back to storage, and re-render the component when the value in storage changes, so you get stable references for free.
250
+
251
+
### `useState` initializer vs `useMemo` for one-time computations
252
+
253
+
When you need to compute something once on mount and never recompute it, `useState(() => ...)` is the right tool - the [initializer function runs only on the first render](https://react.dev/reference/react/useState#avoiding-recreating-the-initial-state). `useMemo` recalculates when dependencies get new references, which can trigger the very re-renders you're trying to avoid. The lazy initializer runs once, freezes the result, and is immune to reference instability.
Use `useState(() => ...)` when the value should be pinned to mount time. For reactive derived values, the [React Compiler](react-compiler.md) handles memoization automatically in compiler-enabled code; only reach for manual `useMemo` when the compiler is not active.
264
+
265
+
See also: [useState lazy initialization](react-compiler.md#alternative-usestate-lazy-initialization) in the React Compiler guide for the compiler-specific motivation.
266
+
267
+
### Stable reducer sentinel values
268
+
269
+
React [skips re-rendering when the new state is identical to the current state](https://react.dev/reference/react/useReducer#dispatch) via `Object.is` comparison. If a reducer returns a new object with the same shape on every dispatch, that check fails and React re-renders. Hoisting fixed sentinel states to module scope ensures the same reference is returned, letting React bail out.
270
+
271
+
Combined with a [render-time read of external state](#dont-read-external-state-during-render), an unstable reducer result is enough to close a render loop: dispatch → reducer returns a new ref → re-render → render-read produces new refs → effect re-fires → dispatch again. Fixing one end stops the loop, but it's worth fixing both: defense-in-depth against a regression on either side.
272
+
273
+
This is only worth investigating if profiling shows unnecessary re-renders from a reducer - React's render batching already handles most cases. Only use sentinels for truly fixed values; states that carry variable data (like errors with messages) need a new object each time.
-**Never import `useDispatch` / `useSelector` from `react-redux`** - Use `useAppDispatch` / `useAppSelector`
@@ -230,3 +298,6 @@ export function useCurrentViewConfig() {
230
298
-**Selectors must return stable references** - Unstable selectors cause infinite loops in Zustand v5 and wasted renders in Redux; hoist fallback values to module scope
231
299
-**Use `useShallow` for multi-value Zustand selectors** - Prefer atomic selectors, but when multiple values are always consumed together, `useShallow` prevents reference instability
232
300
-**Don't suppress dev-mode stability checks** - Reselect and React-Redux stability warnings catch real bugs; fix the root cause instead
301
+
-**Don't read external state during render** - `localStorage`, `sessionStorage`, and browser APIs produce unstable references; read once via `useState(() => ...)` or in effects
302
+
-**Use `useState` initializer for mount-time values** - `useState(() => ...)` runs once and is immune to reference instability. For reactive derived values, the [React Compiler](react-compiler.md) handles memoization automatically in compiler-enabled code; only reach for `useMemo` when the compiler is not active
303
+
-**Consider stable reducer sentinels when profiling shows wasted renders** - Hoisting fixed states (loading) to module scope lets `useReducer` return the same reference so React can bail out; only worth doing when the sub-tree is expensive
0 commit comments