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
+67Lines changed: 67 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -220,6 +220,70 @@ 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 also create new references on every call, which causes render loops and stale-value bugs.
228
+
229
+
```typescript
230
+
// Bad: reads localStorage on every render, creates new arrays each time
231
+
function useActivityFilters(urlParams:URLSearchParams) {
### `useState` initializer vs `useMemo` for one-time computations
250
+
251
+
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.
262
+
263
+
See also: [useState lazy initialization](react-compiler.md#alternative-usestate-lazy-initialization) in the React Compiler guide for the compiler-specific motivation.
264
+
265
+
### Stable reducer sentinel values
266
+
267
+
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.
268
+
269
+
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 +294,6 @@ export function useCurrentViewConfig() {
230
294
-**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
295
-**Use `useShallow` for multi-value Zustand selectors** - Prefer atomic selectors, but when multiple values are always consumed together, `useShallow` prevents reference instability
232
296
-**Don't suppress dev-mode stability checks** - Reselect and React-Redux stability warnings catch real bugs; fix the root cause instead
297
+
-**Don't read external state during render** - `localStorage`, `sessionStorage`, and browser APIs produce unstable references; read once via `useState(() => ...)` or in effects
298
+
-**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
299
+
-**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