| | |

Redux Toolkit 04 ⚛️ Reading State — Accessing store data with useSelector

⏱️ Estimated read: 12 min | |

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

PatternCodeRe-renders When
PrimitiveuseAppSelector((s) => s.counter.value)Value changes
ActionuseAppSelector((s) => s.counter.increment)Never
Object with shallowEqualuseAppSelector((s) => ({ a, b }), shallowEqual)a or b changes
Memoized deriveduseAppSelector(selectFiltered)Inputs change
ParameterizeduseAppSelector((s) => selectById(s, id))id or item changes

Import Sources

ImportPackage
useAppSelectorapp/hooks
shallowEqualreact-redux
createSelector@reduxjs/toolkit

When to Use Each

Use CasePattern
Single valuePrimitive selector
Multiple values, used togetherObject + shallowEqual
Multiple values, used separatelyMultiple hook calls
Derived listcreateSelector
Derived value with parameterFactory 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

PitfallWhy It HappensFix
Component re-renders on every actionSelector returns new object/arrayUse shallowEqual or createSelector
Derived list flickersNo memoization on filtercreateSelector
Selector does not updateSelected value is not changedVerify the selector path
Stale value in parameterized selectorSelector created inside componentUse useCallback or a factory
shallowEqual not importedImported from wrong packageimport { shallowEqual } from 'react-redux'
Entire state re-rendersSelecting (s) => sSelect 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

ItemValue
HookuseAppSelector (typed) or useSelector (plain)
ComparisonReference equality (===) by default
Object selector fixshallowEqual as second argument
Derived array fixcreateSelector from Reselect
Action selectorStable reference; never triggers re-render
Multiple valuesPrefer multiple hook calls
shallowEqual importreact-redux
createSelector import@reduxjs/toolkit

Key takeaways:

  • useSelector uses 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 to useSelector accepts a comparison function. shallowEqual compares top-level properties and prevents re-renders when the contents are unchanged.
  • Derived data requires memoization. createSelector caches 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 useCallback to 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.


Was this helpful?


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!