In React-Redux, why does useSelector(state => ({ count: state.count, user: state.user })) re-render its component after every dispatched action?
answer
- a fresh object every run
- strict equality by default
- split it, or compare shallowly
- a development warning on first call
basics
~20 sThe selector builds a new object on every run, and useSelector compares results with === by default, so after any dispatch the result looks changed and the component re-renders. Select each field separately, or pass shallowEqual as the equality function.
solid answer
~40 sAfter each dispatch, `useSelector` re-runs the selector and compares the new result with the last one using strict `===`. An **object literal** is a new reference every time, so the comparison always fails and the component re-renders on **every** action, even ones unrelated to `count` or `user`. Three fixes: call `useSelector` once per field, since `state.count` and `state.user` return existing references; pass `shallowEqual` from `react-redux` as the second argument, which compares the object's fields one level deep; or use a memoised selector that returns the same object until its inputs change. React-Redux 9 flags the bug in development: on a selector's first call its `stabilityCheck` runs the selector twice and warns *Selector unknown returned a different result when called with the same parameters*.
code
tsx · 24 linesimport { shallowEqual, useSelector } from 'react-redux'
import type { RootState } from './store'
// re-renders after every action: new object each run
function Header() {
const { count, user } = useSelector((s: RootState) => ({ count: s.count, user: s.user }))
return <p>{user.name}: {count}</p>
}
// fix 1: one selector per value
function HeaderSplit() {
const count = useSelector((s: RootState) => s.count)
const user = useSelector((s: RootState) => s.user)
return <p>{user.name}: {count}</p>
}
// fix 2: compare fields shallowly
function HeaderShallow() {
const { count, user } = useSelector(
(s: RootState) => ({ count: s.count, user: s.user }),
shallowEqual,
)
return <p>{user.name}: {count}</p>
}go deeper
Remember that useSelector compares with === by default and that an object literal is a new value on every run.
Explain the compare step after each dispatch, then choose between separate selectors, shallowEqual and a memoised selector.
Use the stabilityCheck and identityFunctionCheck warnings, set them per call or on Provider, and audit selectors that return whole slices or derived arrays.
Make selector stability a review and lint concern so that re-render cost stays predictable as more components subscribe to the store.
## The symptom A component reads two values with one selector: ```tsx const { count, user } = useSelector((state: RootState) => ({ count: state.count, user: state.user, })) ``` It renders correctly, but profiling shows it re-rendering after every action in the app — a keystroke in an unrelated form, a websocket message, a timer tick. ## Why it happens 1. React-Redux subscribes each `useSelector` call to the store. 2. After every dispatch it runs the selector against the new state. 3. It compares the new result with the previous one using the **equality function**, which defaults to strict reference equality, `===`. 4. The arrow function returns an object literal, so every run produces a **new object**, even if `count` and `user` are unchanged. 5. `newObject === oldObject` is `false`, so the component is scheduled to re-render. The values inside did not change; only the wrapper's reference did. ## Three fixes | Fix | How | Trade-off | |---|---|---| | One `useSelector` per value | `const count = useSelector(selectCount)` and `const user = useSelector(selectUser)` | simplest; each call returns an existing reference | | `shallowEqual` | `useSelector(selectBoth, shallowEqual)` or `{ equalityFn: shallowEqual }` | compares each top-level field with `===`; a nested new object still counts as changed | | Memoised selector | a selector that returns the same object until `count` or `user` change | reusable across components; see Reselect for the memoisation details | Multiple `useSelector` calls in one component are fine. Each is its own subscription, and when one dispatch changes several of them, React batches the updates into a single re-render. `shallowEqual` is exported by `react-redux`. It checks that both values have the same keys and that each key's value is `===`. It does **not** compare deeper, so `{ user: { ...state.user } }` would still look changed. ## React-Redux 9's development checks React-Redux runs two checks in development, by default once for each mounted `useSelector` call, on its first run: - **`stabilityCheck`** calls the selector a second time with the same state and warns if the two results are not equal under the equality function in use: *"Selector unknown returned a different result when called with the same parameters. This can lead to unnecessary rerenders."* `unknown` stands in for the selector's function name when it has none, as with an inline arrow; a named selector makes the warning point at it. - **`identityFunctionCheck`** warns when the selector returns the **entire root state**, as `useSelector(state => state)` does, because that re-renders on any change anywhere. Both accept `'once'`, `'always'` or `'never'`, set globally as `Provider` props or per call through `useSelector(selector, { devModeChecks: { stabilityCheck: 'never' } })`. Neither runs in production. ## Related traps - **Returning `state` or a whole slice**: `useSelector(state => state.cart)` re-renders when anything in the cart slice changes, even fields the component does not show. Select the narrowest value the component renders. - **Deriving arrays**: `state.items.filter(...)` has the same problem as an object literal; the result is new every run. - **Expensive selectors**: the selector runs after every dispatch for every mounted `useSelector`, so keep it cheap or memoise it. ## Why not pass shallowEqual everywhere `shallowEqual` looks like a universal fix, but it has costs: - it performs a key-by-key comparison after **every** dispatch for every call site that uses it; - it hides the underlying problem, a selector that allocates a new object on each run; - it silently stops working when one field is itself a freshly built object or array. For a single value, `===` on an existing reference is both cheaper and clearer. Reserve `shallowEqual` for the cases where several values genuinely need to be returned together. ## How to verify the fix - Watch the development console for the stability warning to disappear. - Use a profiler or render logging to confirm the component no longer renders on unrelated actions. - In tests, dispatch an unrelated action and assert the component's render count is unchanged. An interview answer should name the new reference, the default `===` comparison, at least two fixes, and the development warning that catches it.
- Does passing shallowEqual to React-Redux's useSelector fix a selector returning { user: { ...state.user } }?No. `shallowEqual` compares each top-level field with `===`, and the spread creates a new `user` object every run, so that field always differs. Return `state.user` itself, or memoise the derived object.
- What does React-Redux's identityFunctionCheck warn about?It warns in development when a selector returns the entire root state, as `useSelector(state => state)` does. Such a component re-renders whenever anything in the store changes, which is almost always a mistake.
saying these in an interview costs you the question
- useSelector compares results with shallow equality by default
- Calling useSelector several times in one component is an anti-pattern
- shallowEqual compares nested objects recursively
- The stability warning also fires in production builds
- The re-render only happens when count or user change