Redux Toolkit 04 ⚛️ Reading State — Accessing store data with useSelector
The store holds the state, and slices define its shape. Reading that state in a component is the job of useSelector. The hook accepts a selector function, calls it with the current state, and subscribes the component to the result. When the store updates, the selector runs again, and the component re-renders only if the returned value changed according to reference equality. This chapter covers the selector patterns that make components efficient: selecting individual fields, selecting derived data, avoiding unnecessary re-renders, and using the typed useAppSelector hook from the previous chapters.
Key point: useSelector uses reference equality (===) to decide whether the selected value changed. Returning a new object or array from a selector—(state) => ({ a: state.a, b: state.b }) or (state) => state.items.filter(...)—causes a re-render on every store update, because the new reference never equals the previous one. The fix is to select primitives individually, use the shallow-equal option from react-redux, or memoize the selector with createSelector from Reselect.
Why selector design determines render behavior
The reference-equality problem.
React Redux compares the previous selector result with the new one using strict equality. If a selector returns a primitive—a number, string, or boolean—the comparison is meaningful: the component re-renders only when the value actually changes. If the selector returns a new object or array, the comparison always fails, and the component re-renders on every dispatched action, even those that do not affect its state . This is the most common performance bug in Redux-connected components.
The derived-data problem.
A component often needs a filtered, sorted, or computed view of the state rather than the raw state itself. Computing that view inline in the selector—(state) => state.todos.filter(...)—produces a new array on every call. The component re-renders even when the underlying todos have not changed. The solution is a memoized selector, created with createSelector from Reselect, which caches the result and recomputes only when the input changes .
The stale-closure problem.
A selector that captures a value from the component’s scope—(state) => state.items[props.index]—is re-created on every render. React Redux can handle this, but if the selector is passed to useSelector inline, the subscription may behave unexpectedly when the captured value changes. The recommended pattern is to pass the captured value through the selector’s second argument or to define the selector outside the component and use props only as inputs to a factory .
a. Selecting primitives with useAppSelector
The typed useAppSelector hook from the store setup provides the full RootState type without annotations. Each call selects one value.
// src/features/counter/Counter.tsx
import { useAppSelector } from '../../app/hooks'
function CounterDisplay() {
// Select one primitive; re-renders only when value changes
const value = useAppSelector((state) => state.counter.value)
return <p>Count: {value}</p>
}
function StepDisplay() {
// Independent subscription to a different slice field
const step = useAppSelector((state) => state.counter.step)
return <p>Step: {step}</p>
}
Each hook call creates a separate subscription. CounterDisplay re-renders only when value changes; StepDisplay re-renders only when step changes. The independence is the core benefit of selecting primitives individually .
When a component needs multiple values, the recommended pattern is multiple hook calls rather than one selector returning an object.
function CounterSummary() {
// Two independent subscriptions
const value = useAppSelector((state) => state.counter.value)
const step = useAppSelector((state) => state.counter.step)
return <p>{value} (step {step})</p>
}
React Redux is optimized for multiple useSelector calls in the same component. Each subscription is independent, and the component re-renders when any of them change. The alternative—returning an object from one selector—requires shallowEqual to avoid unnecessary re-renders.
b. The shallowEqual option and object selectors
When an object selector is necessary, React Redux accepts a comparison function as the second argument to useSelector. The shallowEqual function compares each top-level property with strict equality.
import { shallowEqual } from 'react-redux'
import { useAppSelector } from '../../app/hooks'
function CartHeader() {
// Returns an object; shallowEqual compares each property
const { count, total } = useAppSelector(
(state) => ({
count: state.cart.items.length,
total: state.cart.total,
}),
shallowEqual
)
return <h2>{count} items, ${total}</h2>
}
The shallowEqual comparison checks count === prevCount and total === prevTotal. If both are equal, the component does not re-render, even though the object reference is new. The comparison is one level deep; nested objects are compared by reference and will still cause re-renders unless they are the same reference .
The typed version of useAppSelector accepts the comparison function as its second argument, so the same pattern applies with full type safety.
const { count, total } = useAppSelector(
(state) => ({ count: state.cart.items.length, total: state.cart.total }),
shallowEqual
)
The alternative to shallowEqual is to select each value with a separate hook call. Both patterns are correct; the choice depends on whether the object is used as a unit or the values are used independently.
c. Memoized selectors with createSelector
For derived data—filtered lists, sorted arrays, computed totals—the selector must not recompute on every call unless its inputs changed. createSelector from Reselect creates a memoized selector that caches the result and recomputes only when the input selectors return new values.
// src/features/todos/todosSelectors.ts
import { createSelector } from '@reduxjs/toolkit'
import type { RootState } from '../../app/store'
// Input selectors: select raw state slices
const selectTodos = (state: RootState) => state.todos.items
const selectFilter = (state: RootState) => state.todos.filter
// Memoized selector: recomputes only when todos or filter changes
export const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'active':
return todos.filter((todo) => !todo.done)
case 'completed':
return todos.filter((todo) => todo.done)
default:
return todos
}
}
)
The createSelector call takes an array of input selectors and a result function. The result function runs only when one of the inputs changes. The returned array is the same reference between renders if the inputs have not changed, so useSelector does not trigger a re-render .
function TodoList() {
// The memoized selector returns a stable array
// Re-renders only when the filtered result actually changes
const filteredTodos = useAppSelector(selectFilteredTodos)
return (
<ul>
{filteredTodos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)
}
The memoization is what makes the derived array stable. Without createSelector, the filter runs on every store update and returns a new array, causing a re-render even when the filtered contents are identical. With createSelector, the result is cached by input reference, and the component re-renders only when the filter or the todos array changes.
createSelector also supports parameterized selectors for cases where the derived value depends on a prop. The factory pattern creates a new selector instance per parameter, but this requires careful memoization to avoid recreating the selector on every render.
Complete Example Session
// ============================================
// PART 1: PRIMITIVE SELECTORS
// ============================================
// src/features/cart/CartBadge.tsx
import { useAppSelector } from '../../app/hooks'
function CartBadge() {
// Primitive: re-renders only when count changes
const count = useAppSelector((state) => state.cart.items.length)
return <span className="badge">{count}</span>
}
// ============================================
// PART 2: MULTIPLE PRIMITIVE SELECTORS
// ============================================
function CartTotal() {
// Two independent subscriptions
const subtotal = useAppSelector((state) => state.cart.subtotal)
const tax = useAppSelector((state) => state.cart.tax)
return <p>Total: ${(subtotal + tax).toFixed(2)}</p>
}
// ============================================
// PART 3: ACTION SELECTOR
// ============================================
function CartControls() {
// Actions are stable references; never re-renders
const clearCart = useAppSelector((state) => state.cart.clearCart)
return <button onClick={clearCart}>Clear</button>
}
// ============================================
// PART 4: OBJECT SELECTOR WITH shallowEqual
// ============================================
import { shallowEqual } from 'react-redux'
function CartHeader() {
// Object selector; shallowEqual prevents re-renders
// when count and subtotal are unchanged
const { count, subtotal } = useAppSelector(
(state) => ({
count: state.cart.items.length,
subtotal: state.cart.subtotal,
}),
shallowEqual
)
return <h2>{count} items — ${subtotal}</h2>
}
// ============================================
// PART 5: MEMOIZED DERIVED SELECTOR
// ============================================
// src/features/cart/cartSelectors.ts
import { createSelector } from '@reduxjs/toolkit'
import type { RootState } from '../../app/store'
const selectItems = (state: RootState) => state.cart.items
export const selectExpensiveItems = createSelector(
[selectItems],
(items) => items.filter((item) => item.price > 100)
)
// ============================================
// PART 6: USING THE MEMOIZED SELECTOR
// ============================================
function ExpensiveItems() {
// Stable array reference between renders
const expensive = useAppSelector(selectExpensiveItems)
return (
<ul>
{expensive.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}
// ============================================
// PART 7: PARAMETERIZED SELECTOR
// ============================================
// A selector that takes an id as a parameter
// Use a factory or pass the id through a closure with useCallback
const selectItemById = (state: RootState, id: number) =>
state.cart.items.find((item) => item.id === id)
function CartItemName({ id }: { id: number }) {
const item = useAppSelector((state) => selectItemById(state, id))
return <span>{item?.name ?? 'Unknown'}</span>
}
The seven parts demonstrate the full range of selector patterns: primitive selectors, multiple subscriptions, action selectors, object selectors with shallowEqual, memoized derived selectors, and parameterized selectors. The memoized selector in Part 5 is the one that prevents the most common re-render bug in list-rendering components.
Quick Reference
Selector Patterns
| Pattern | Code | Re-renders When |
|---|---|---|
| Primitive | useAppSelector((s) => s.counter.value) | Value changes |
| Action | useAppSelector((s) => s.counter.increment) | Never |
Object with shallowEqual | useAppSelector((s) => ({ a, b }), shallowEqual) | a or b changes |
| Memoized derived | useAppSelector(selectFiltered) | Inputs change |
| Parameterized | useAppSelector((s) => selectById(s, id)) | id or item changes |
Import Sources
| Import | Package |
|---|---|
useAppSelector | app/hooks |
shallowEqual | react-redux |
createSelector | @reduxjs/toolkit |
When to Use Each
| Use Case | Pattern |
|---|---|
| Single value | Primitive selector |
| Multiple values, used together | Object + shallowEqual |
| Multiple values, used separately | Multiple hook calls |
| Derived list | createSelector |
| Derived value with parameter | Factory or closure |
Best Practices
✅ Do This:
// Select primitives individually
const value = useAppSelector((s) => s.counter.value) // ✅
// Use shallowEqual for object selectors
const { a, b } = useAppSelector((s) => ({ a: s.a, b: s.b }), shallowEqual) // ✅
// Memoize derived arrays with createSelector
export const selectFiltered = createSelector([selectItems], ...) // ✅
// Select actions individually
const increment = useAppSelector((s) => s.counter.increment) // ✅
// Use the typed hook
const value = useAppSelector((s) => s.counter.value) // ✅
❌ Don’t Do This:
// Return a new object without shallowEqual
const { a, b } = useAppSelector((s) => ({ a: s.a, b: s.b })) // ❌ Re-renders always
// Filter in the selector without memoization
const done = useAppSelector((s) => s.todos.filter((t) => t.done)) // ❌ New array
// Select the entire state
const state = useAppSelector((s) => s) // Re-renders on any change // ❌
// Use plain useSelector without types
const value = useSelector((s: RootState) => s.counter.value) // ❌ Use typed hook
// Create the selector inside the component
useAppSelector((s) => s.items.filter(...)) // No memoization // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Component re-renders on every action | Selector returns new object/array | Use shallowEqual or createSelector |
| Derived list flickers | No memoization on filter | createSelector |
| Selector does not update | Selected value is not changed | Verify the selector path |
| Stale value in parameterized selector | Selector created inside component | Use useCallback or a factory |
shallowEqual not imported | Imported from wrong package | import { shallowEqual } from 'react-redux' |
| Entire state re-renders | Selecting (s) => s | Select specific slices |
Real-World Examples
1. Primitive selector
const value = useAppSelector((state) => state.counter.value)
2. Action selector
const increment = useAppSelector((state) => state.counter.increment)
3. Multiple hooks
const value = useAppSelector((s) => s.counter.value)
const step = useAppSelector((s) => s.counter.step)
4. Object selector with shallowEqual
const { a, b } = useAppSelector((s) => ({ a: s.a, b: s.b }), shallowEqual)
5. Memoized filtered list
export const selectDone = createSelector([selectTodos], (todos) => todos.filter((t) => t.done))
6. Parameterized selector
const item = useAppSelector((s) => s.items.find((i) => i.id === id))
7. Derived total
export const selectTotal = createSelector([selectItems], (items) => items.reduce((s, i) => s + i.price, 0))
8. Selector with shallowEqual for state object
const { name, email } = useAppSelector((s) => ({ name: s.user.name, email: s.user.email }), shallowEqual)
9. Derived count with primitive result
const count = useAppSelector((s) => s.todos.items.length)
10. Selector for boolean
const isOpen = useAppSelector((s) => s.ui.isOpen)
Visual
Selector Reference Equality
┌──────────────────────────────────────────────────────────────┐
│ REFERENCE EQUALITY DECIDES WHETHER TO RE-RENDER │
│ │
│ Primitive selector: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ (s) => s.counter.value │ │
│ │ Before: 5 After: 5 → 5 === 5 → no re-render │ │
│ │ Before: 5 After: 6 → 5 !== 6 → re-render │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Object selector without shallowEqual: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ (s) => ({ a: s.a, b: s.b }) │ │
│ │ Before: { a: 1, b: 2 } ← reference A │ │
│ │ After: { a: 1, b: 2 } ← reference B │ │
│ │ A !== B → re-render (even though contents are equal) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Object selector with shallowEqual: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ shallowEqual(A, B) → compares a and b individually │ │
│ │ a: 1 === 1 ✓ b: 2 === 2 ✓ │ │
│ │ → no re-render │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Memoized Selector Flow
┌──────────────────────────────────────────────────────────────┐
│ createSelector CACHES THE DERIVED RESULT │
│ │
│ selectFilteredTodos = createSelector( │
│ [selectTodos, selectFilter], │
│ (todos, filter) => todos.filter(...) │
│ ) │
│ │
│ Store update: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ selectTodos(state) → same array reference? │ │
│ │ selectFilter(state) → same string value? │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────┴────────────┐ │
│ │ │ │
│ ▼ both same ▼ either changed │
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
│ │ Return cached result │ │ Run filter function │ │
│ │ No re-render │ │ Return new array │ │
│ │ │ │ Re-render component │ │
│ └──────────────────────────┘ └──────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Selector Granularity
┌──────────────────────────────────────────────────────────────┐
│ FINE-GRAINED SELECTORS MINIMIZE RE-RENDERS │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Store: { counter: { value, step }, │ │
│ │ todos: { items, filter }, │ │
│ │ user: { name, email } } │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Component A: useAppSelector((s) => s.counter.value) │
│ → re-renders only when counter.value changes │
│ │
│ Component B: useAppSelector((s) => s.todos.items) │
│ → re-renders only when todos.items changes │
│ │
│ Component C: useAppSelector((s) => s.user.name) │
│ → re-renders only when user.name changes │
│ │
│ Component D: useAppSelector((s) => s) │
│ → re-renders on ANY change in the store │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Hook | useAppSelector (typed) or useSelector (plain) |
| Comparison | Reference equality (===) by default |
| Object selector fix | shallowEqual as second argument |
| Derived array fix | createSelector from Reselect |
| Action selector | Stable reference; never triggers re-render |
| Multiple values | Prefer multiple hook calls |
shallowEqual import | react-redux |
createSelector import | @reduxjs/toolkit |
Key takeaways:
useSelectoruses reference equality. A selector returning a primitive re-renders only when the value changes. A selector returning a new object or array re-renders on every store update unless the comparison is changed .- Object selectors require
shallowEqual. The second argument touseSelectoraccepts a comparison function.shallowEqualcompares top-level properties and prevents re-renders when the contents are unchanged. - Derived data requires memoization.
createSelectorcaches the result and recomputes only when the input selectors return new values. Without it, a filter or sort in the selector produces a new array on every call and defeats the subscription . - Multiple hook calls are preferred for multiple primitives. Each call creates an independent subscription, and React Redux is optimized for this pattern. The object-selector pattern is for values that are used as a unit.
- Action selectors never cause re-renders. Actions are functions created once by
createSlice. Their reference is stable, so a component that only selects actions never re-renders after mount. - Parameterized selectors need care. A selector that captures a prop is re-created on every render. Use a factory or
useCallbackto keep the selector stable.
Remember: useSelector is a subscription, and the selector is the subscription key. A primitive selector subscribes to one value. An object selector without shallowEqual subscribes to a new reference on every update. A derived selector without createSelector computes a new array on every call. Select the smallest value the component needs, use shallowEqual when the value is an object, and memoize derived data. The component re-renders when the selected value changes, and only then.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!