Home / Articles / How React-Redux Decides to Re-render: Selectors, Equality and Typed Hooks

This article is published in English.

How React-Redux Decides to Re-render: Selectors, Equality and Typed Hooks

A study guide to React-Redux internals: Provider, useSelector equality, memoized selectors, connect(), typed hooks with withTypes(), and rare edge cases.

7738 words

The React-Redux surface area looks tiny: a provider component and a couple of hooks. Yet on a large codebase the same questions keep coming back. Why does this component render on every dispatch? Why does a selector that returns an object behave differently from mapStateToProps? When is shallowEqual the right tool, what do RootState and AppDispatch actually describe, and is batch() still needed on React 18?

This guide answers those questions by following one thread: what happens between a dispatched action and a re-render. Once that path is clear, the individual APIs stop being a list to memorize and become parts of one system you can reason about, debug and explain in a code review or an interview.

useSelector()
useDispatch()
<Provider />

Current guidance: the React-Redux maintainers recommend the Hooks API as the default for components. connect() is still supported and still worth knowing, because a lot of existing code relies on it.

What React-Redux is and where it sits

Redux holds state, runs reducers and notifies listeners; React renders UI. React-Redux is the official binding that lets components read from the store and trigger updates while React stays in charge of rendering.

Redux
  │
  │ Application State
  ▼
React-Redux
  │
  │ Integration
  ▼
React
  │
  │ UI
  ▼
User

Concretely, the binding gives components four abilities:

  • reading values out of the Redux state
  • subscribing to changes of those values
  • dispatching actions to the store
  • hooking store updates into React's rendering cycle

The modern entry points for that work are three hooks:

useSelector()
useDispatch()
useStore()

plus the component that makes the store reachable in the first place:

<Provider />

For older code there is also the higher-order component API, which remains fully supported:

connect()

Redux and React-Redux are different packages

Redux itself is the state container and owns these concepts:

Store
State
Actions
Reducers
Dispatch
Middleware
Selectors

React-Redux is only the bridge, and everything it exports is about talking to React:

Provider
useSelector
useDispatch
useStore
connect

Stacked together, the layers look like this:

              Redux
        ┌───────────────┐
        │ Store         │
        │ State         │
        │ Reducers      │
        │ Actions       │
        │ Middleware    │
        └───────┬───────┘
                │
                ▼
          React-Redux
        ┌───────────────┐
        │ Provider      │
        │ useSelector   │
        │ useDispatch   │
        │ useStore      │
        │ connect       │
        └───────┬───────┘
                │
                ▼
              React

If a question is about how state changes (reducers, middleware, action shapes), it belongs to Redux. If it is about when a component sees a change or renders, it belongs to React-Redux.

The core loop to keep in your head

Before any single API, internalize the cycle: read through a selector, dispatch on interaction, reduce to new state, re-run the selector, and render only if the selected value changed.

                 React Component
                       │
                       │ useSelector()
                       ▼
                 Redux Store
                       │
                       │ current state
                       ▼
                    Component
                       │
                       │ user interaction
                       ▼
                 useDispatch()
                       │
                       │ dispatch(action)
                       ▼
                    Reducer
                       │
                       ▼
                  New Redux State
                       │
                       ▼
                 useSelector()
                       │
                       ▼
                Re-render if
                 selected value
                   changed

Almost every performance question about React-Redux is a question about the last step of that diagram.

Provider: making the store reachable

Hooks can only talk to a store that is available somewhere above them in the tree. <Provider> is what puts it there. You typically wrap the root of the app once:

import { Provider } from 'react-redux'
import { store } from './store'
function AppRoot() {
  return (
    <Provider store={store}>
      <App />
    </Provider>
  )
}

Under the hood, Provider places the store into a React context. Any descendant, however deep, can then reach it without prop drilling:

<Provider store={store}>
       │
       ├── App
       │    ├── Header
       │    ├── Dashboard
       │    ├── UserProfile
       │    └── Settings
       │
       └── Every descendant
            can use Redux

If a component calls one of the hooks outside a Provider, there is no store to read from and the hook fails (in practice it throws an error telling you the context value is missing):

useSelector(...)
useDispatch(...)

A common place this bites is in unit tests that render a component without wrapping it in a test Provider.

useSelector: how components read state

useSelector() is the hook you will call most. You give it a function that receives the whole Redux state and returns the piece the component needs:

const count = useSelector(
  state => state.counter.value
)

The shape of the data flow is simple:

Redux State
     │
     ▼
useSelector()
     │
     ▼
Selected Value
     │
     ▼
React Component

Because React-Redux may call your selector more often than you expect (on render, after dispatches, during development checks), the selector has to be pure: same input, same output, no side effects.

What the hook does on every render and dispatch

Take a selector that picks the current user:

const user = useSelector(
  state => state.auth.user
)

Behind that one line, React-Redux performs a small routine:

  1. reads the current store state
  2. calls your selector with it
  3. keeps the returned value as the "last selected" result
  4. registers a subscription to the store for this component
  5. re-runs the selector after each dispatched action
  6. compares the previous result to the new one
  7. schedules a re-render only when the comparison says they differ

Step 6 is where nearly all surprising behavior comes from.

The default comparison is strict reference equality

Out of the box, the hook

useSelector()

compares results with ===:

previousResult === newResult

When the comparison yields

true

the selector gives React-Redux no reason to render the component again. When it yields

false

the component is scheduled to re-render.

This is a deliberate difference from connect(), which shallow-compares the object returned by mapStateToProps. Code that was fine under connect() can start rendering on every action once it is ported to hooks without adjusting the selector.

Why returning a fresh object re-renders every time

A natural first attempt at reading two values looks like this:

const data = useSelector(state => ({
  user: state.user,
  count: state.count
}))

Nothing is wrong with the values, but the arrow function builds a brand-new object literal each time it runs:

{
  user: ...,
  count: ...
}

Two object literals with identical contents are still two different objects, so the check

oldObject === newObject

always evaluates to

false

The consequence is a chain that fires on every action in the app, including actions that have nothing to do with users or counts:

Action dispatched
      ↓
Selector executes
      ↓
New object created
      ↓
Different reference
      ↓
Component re-renders

This is among the most frequent React-Redux performance bugs. The documentation lists three ways out: separate selectors, a custom equality function such as shallowEqual, or a memoized selector.

Fix one: call useSelector once per value

Rather than bundling values into an object,

const data = useSelector(state => ({
  user: state.user,
  count: state.count
}))

split the read into independent hooks:

const user = useSelector(
  state => state.user
)
const count = useSelector(
  state => state.count
)

Each selector now returns a value that already lives in the store, not a new wrapper around it. When the state that holds them has not changed

user unchanged
count unchanged

the references are identical and the === check passes.

There is no penalty for calling the hook several times in one component. If a single dispatch changes more than one of the selected values, React-Redux batches the resulting updates, so the component still renders once for that dispatch rather than once per hook.

Fix two: shallowEqual for intentional objects

Sometimes an object really is the clearest thing to return, for example when a component consumes a small bundle of related fields. In that case pass shallowEqual as the comparison:

import {
  useSelector,
  shallowEqual
} from 'react-redux';

const data = useSelector(
  state => ({
    user: state.user,
    count: state.count
  }),
  shallowEqual
)

With it, React-Redux compares each top-level field of the old and new objects with === instead of comparing the two object references. A new wrapper with the same field values counts as unchanged.

Recent versions also accept the comparison through an options object:

const data = useSelector(
  selector,
  {
    equalityFn: shallowEqual
  }
)

When shallowEqual is worth it

Reach for shallowEqual when grouping values makes the component easier to read. Do not treat it as a default that gets bolted onto every call:

useSelector(selector, shallowEqual)

The better first question is whether the component could simply select each value on its own. Most of the time it can, and the result is easier to scan:

const user = useSelector(
  state => state.auth.user
)

const permissions = useSelector(
  state => state.auth.permissions
)

Keep in mind that shallowEqual only looks one level deep. If a field inside the returned object is itself a freshly built array or object, the comparison still fails.

Selectors that derive data

Selectors do more than pluck fields. They are also where you compute derived data, such as a filtered list:

const selectCompletedTodos = state =>
  state.todos.filter(
    todo => todo.completed
  )

The catch is that

filter()

always allocates a new array. Even when the todo list has not changed in any meaningful way,

oldArray !== newArray

holds, and the component renders after every action. Memoization solves this by caching the output against the inputs. When the inputs are the same objects as last time, you get the cached result back:

Same inputs
    ↓
Return cached result

When an input has changed, the calculation runs again:

Changed inputs
    ↓
Recalculate

createSelector from Reselect

The standard tool for this is createSelector, from Reselect (Redux Toolkit re-exports it). You list input selectors, then a result function that only runs when those inputs change:

import { createSelector } from 'reselect';

const selectCompletedTodos = createSelector(
  state => state.todos,
  todos =>
    todos.filter(todo => todo.completed)
)

The component consumes it like any other selector:

const todos = useSelector(
  selectCompletedTodos
)

While state.todos is the same array reference, the selector returns its previous filtered array, so === passes. The caveat: a memoized selector carries a cache, so where its instance is created matters.

Create state-only memoized selectors once, at module level

When a memoized selector depends only on the Redux state,

const selectCompletedTodos = createSelector(
  state => state.todos,
  todos => ...
)

declare it outside any component:

const selectCompletedTodos = createSelector(...)

and reference it from the component:

function TodoList() {
  const todos = useSelector(
    selectCompletedTodos
  )
}

If createSelector ran inside the component body, every render would create a new selector with an empty cache, and memoization would never help. A module-level instance survives across renders.

Memoized selectors that also need props

Plain, non-memoized selectors that read props are harmless. This one keeps no cache, so there is nothing to get wrong:

function TodoListItem({ id }) {
  const todo = useSelector(
    state => state.todos[id]
  )
return <div>{todo.text}</div>
}

Things get subtler once a memoized selector depends on both

Redux State + Component Props

Such a selector caches the result for its last arguments. If many list items share one instance, each with a different id, they keep invalidating each other's cache. For one component, creating the selector with useMemo is usually enough; for many, you need to know your library's memoization strategy (cache size, or one instance per component). This comes up in senior interviews and in long lists.

Selectors must stay pure

A selector should be a plain function of state:

State
  ↓
Selector
  ↓
Value

and never a place where work leaks out into the world:

State
  ↓
Selector
  ↓
API call
  ↓
Mutation
  ↓
Side effect

The following is the kind of selector to avoid; logging, network calls and mutations do not belong here:

const selectUser = state => {
  console.log('side effect')
  // API call ❌
  // mutation ❌
  return state.user
}

Using props inside a selector

A selector defined inline in a component can simply close over the component's props:

function TodoItem({ id }) {
  const todo = useSelector(
    state => state.todos[id]
  )
return <div>{todo.text}</div>
}

The value travels from the prop into the selector through the closure:

id
 ↓
closure
 ↓
selector
 ↓
state.todos[id]

This is a real difference from mapStateToProps, which receives ownProps as a second argument. useSelector() passes no props at all, so you rely on closures or on selector factories that take extra arguments.

useDispatch: sending actions

If useSelector() is the read side,

useSelector
     ↓
    READ

then useDispatch() is the write side:

useDispatch
     ↓
  DISPATCH

It returns the store's dispatch function, which you call from event handlers:

const dispatch = useDispatch()

function handleClick() {
  dispatch(increment())
}

or inline in JSX:

<button
  onClick={() => dispatch(increment())}
>
  Increment
</button>

What a dispatch sets in motion

Tracing a click end to end reinforces the core loop from earlier:

User clicks button
       ↓
dispatch(action)
       ↓
Redux
       ↓
Reducer
       ↓
New state
       ↓
useSelector()
       ↓
Component updates

A concrete call with an action creator and a payload looks like this:

dispatch(
  addTodo({
    id: 1,
    text: 'Learn Redux'
  })
)

Stable callbacks for memoized children

Most dispatch callbacks do not need useCallback. The exception is when a handler such as

const increment = () =>
  dispatch(incrementAction())

is passed down to a child that is wrapped in React.memo:

<MyButton
  onIncrement={increment}
/>

Because the arrow function is recreated on every render of the parent, the memoized child sees a new prop each time and renders anyway. Wrapping the handler fixes that:

const increment = useCallback(
  () => dispatch(incrementAction()),
  [dispatch]
)

together with a memoized child:

const MyButton = React.memo(...)

Listing dispatch as a dependency is safe: its identity stays the same as long as the Provider receives the same store instance.

useStore: direct access, rarely needed

The third hook hands you the store object itself:

const store = useStore()

With it you can call the raw store methods:

store.getState()
store.dispatch(...)
store.subscribe(...)

That access is almost never what a component should use for rendering data. Reading goes through

useSelector()

and writing goes through

useDispatch()

The docs treat useStore() as an escape hatch for rare cases, such as injecting a reducer, not for everyday reads.

Why store.getState() in render goes stale

Consider a component that reads from the store directly:

function Component() {
  const store = useStore()
  const user = store.getState().user
  return <div>{user.name}</div>
}

This renders correctly once and then falls behind. getState() is a one-off read with no subscription, so a later change never tells React to re-render this component. The subscribing version stays in sync:

const user = useSelector(
  state => state.user
)

The three hooks at a glance

┌────────────────────────────┐
│ React-Redux Hooks          │
├────────────────────────────┤
│ useSelector()              │ → READ
│ useDispatch()              │ → DISPATCH
│ useStore()                 │ → STORE ACCESS
└────────────────────────────┘

In day-to-day component code, the first two do nearly all of the work:

90%+
useSelector()
useDispatch()

useStore() shows up only occasionally.

connect(): the higher-order component API

Hooks are the recommended path, but connect() has not gone anywhere, and many long-lived codebases are built on it. Expect to meet code like this:

connect(
  mapStateToProps,
  mapDispatchToProps
)(Component)

How connect maps the store into props

The mental model is a wrapper that turns store data and dispatch functions into ordinary props:

Redux Store
     │
     ▼
connect()
     │
     ├── mapStateToProps
     │
     └── mapDispatchToProps
     │
     ▼
Component Props

mapStateToProps receives the state and returns an object of props:

const mapStateToProps = state => ({
  user: state.auth.user,
  count: state.counter.value
})

In effect it performs this translation:

Redux State
     ↓
Component Props

The wrapped component stays a plain function of its props and does not know Redux exists:

function User({ user, count }) {
  return (
    <div>
      {user.name}
      {count}
    </div>
  )
}

mapDispatchToProps supplies the callbacks. In its function form, you receive dispatch and build the handlers yourself:

const mapDispatchToProps =
  dispatch => ({
    increment: () =>
      dispatch(increment())
  })

The component then calls them as props:

props.increment()

The object shorthand for mapDispatchToProps

The more concise form passes an object of action creators:

const mapDispatchToProps = {
  increment,
  decrement
}

React-Redux binds each action creator so calling the prop dispatches it. This is usually the tidier option.

The four common connect shapes

You will run into four variations. With no arguments, the component gets only dispatch as a prop:

connect()(Component)

With only a state mapper, the component reads data but does not receive bound action creators:

connect(
  mapStateToProps
)(Component)

With null as the first argument, the component never listens to the store and only receives dispatch props:

connect(
  null,
  mapDispatchToProps
)(Component)

And with both, it reads and dispatches:

connect(
  mapStateToProps,
  mapDispatchToProps
)(Component)

Knowing that the null form skips the subscription is useful: it is a cheap way to give a component dispatch access without making it render on store updates.

connect returns a new component

Calling

connect(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent)

does not alter MyComponent. It produces a separate wrapper component that renders yours inside it:

MyComponent
     │
     ▼
connect()
     │
     ▼
ConnectedComponent

That is why connected modules usually export the wrapped version as the default and sometimes export the plain component separately for tests.

Choosing between hooks and connect

For a new application, the answer is short:

New React application
        ↓
Hooks

For an existing one, the answer is pragmatic:

Existing connect()
        ↓
Understand and maintain it

Hooks mean less boilerplate, no wrapper components and much easier TypeScript, which is why they are the default. Working connect() components do not need a rewrite; convert them when you touch them anyway.

The equality difference in one line

Keep this pairing in mind:

useSelector()
      ↓
=== reference equality

versus

connect()
      ↓
shallow equality

Many "it worked before the refactor" bugs come from this: a fresh object was safe in mapStateToProps, but fails === in useSelector().

Typing React-Redux with TypeScript

React-Redux ships its own type definitions, and the docs describe a standard typed setup. It revolves around six names:

RootState
AppDispatch
AppStore
useAppSelector
useAppDispatch
useAppStore

RootState: infer the state type from the store

Given a store configured with Redux Toolkit,

const store = configureStore({
  reducer: {
    counter: counterReducer,
    users: usersReducer
  }
})

derive the state type from what getState returns:

export type RootState =
  ReturnType<typeof store.getState>

This beats writing the shape by hand,

type RootState = {
  counter: CounterState
  users: UsersState
}

because the inferred type follows the reducers automatically.

AppDispatch: the dispatch type including middleware

Infer the dispatch type the same way:

export type AppDispatch =
  typeof store.dispatch

The plain Redux Dispatch type only knows plain actions; the inferred one reflects your middleware, which matters once you use:

  • middleware that changes what dispatch accepts
  • thunks
  • a customized dispatch
  • async actions of any kind

Without AppDispatch, dispatching a thunk is a type error.

AppStore: the type of the store itself

The store type is one more inference away:

export type AppStore =
  typeof store

With that, each concern has a single source of truth. The state type:

RootState
    ↓
state type

The dispatch type:

AppDispatch
    ↓
dispatch type

The store type:

AppStore
    ↓
store type

AppStore becomes especially handy when you create a store per request or per test and need to pass it around.

Pre-typed hooks with withTypes()

Starting with React-Redux 9.1.0, each hook exposes a .withTypes() method:

useDispatch.withTypes()
useSelector.withTypes()
useStore.withTypes()

The documented pattern builds application-specific hooks from them once:

export const useAppDispatch =
  useDispatch.withTypes<AppDispatch>()

export const useAppSelector =
  useSelector.withTypes<RootState>()

export const useAppStore =
  useStore.withTypes<AppStore>()

What typed hooks save you

Without them, every selector needs an explicit annotation:

const user = useSelector(
  (state: RootState) =>
    state.auth.user
)

With the typed hook,

const user = useAppSelector(
  state => state.auth.user
)

the compiler already knows

state = RootState

The dispatch side works the same way:

const dispatch = useAppDispatch()

This dispatch accepts thunks and anything else your middleware allows, with full checking.

A hooks.ts file for the app

A typical project keeps these in one small module:

import {
  useDispatch,
  useSelector,
  useStore
} from 'react-redux'

import type {
  RootState,
  AppDispatch,
  AppStore
} from './store'

export const useAppDispatch =
  useDispatch.withTypes<AppDispatch>()

export const useAppSelector =
  useSelector.withTypes<RootState>()

export const useAppStore =
  useStore.withTypes<AppStore>()

Components import from that module instead of from react-redux directly:

const user = useAppSelector(
  state => state.auth.user
)

const dispatch = useAppDispatch()

ConnectedProps for typed connect() code

Codebases that type connect() components will contain

ConnectedProps

Split the connect call into a connector first, then extract the props it injects:

const connector = connect(
  mapState,
  mapDispatch
)

type PropsFromRedux =
  ConnectedProps<typeof connector>

PropsFromRedux describes exactly what the connector injects, so the mapped types are never duplicated.

Designing good selectors

It is tempting to think of a selector as nothing more than

state => state.user

In a larger application, selectors are better seen as a boundary that turns the storage format of your state into the shape the UI wants:

Redux State
     ↓
Selector
     ↓
UI-friendly data

For example, the rule for which todos are visible can live in one named function:

const selectVisibleTodos =
  state =>
    state.todos.filter(
      todo => !todo.hidden
    )

and the component just asks for the result:

const todos = useSelector(
  selectVisibleTodos
)

The component stays presentational, and the rule is testable on its own.

A good selector is precise

const selectUserName =
  state => state.auth.user.name

It returns exactly what the component displays and nothing more, so it only triggers a render when that name changes.

Selecting the whole state is almost always wrong

const selectEverything =
  state => state

Redux produces a new root state object whenever any reducer changes anything. A selector that returns the root therefore returns a new reference after practically every action:

Anything in Redux changes
        ↓
Root state reference changes
        ↓
Selector result changes
        ↓
Component re-renders

React-Redux's development checks flag this pattern.

Keep selectors granular

Prefer several targeted reads:

const count =
  useSelector(
    state => state.counter.value
  )

const user =
  useSelector(
    state => state.auth.currentUser
  )

over one catch-all read:

const state =
  useSelector(state => state)

A handy rule of thumb:

Select the smallest piece of state the component can actually use.

Development-mode selector checks

Recent React-Redux versions run extra checks on your selectors in development builds. Two of them are worth knowing by name.

The stability check

The first check calls your selector a second time with the same state and compares the results:

selector(state)
      ↓
run again with same state
      ↓
same result?

If the outcome is

same reference

the selector is stable. If it is

new reference

React-Redux logs a warning, because a selector that returns a new reference for identical input will re-render its component on every store update.

A typical offender is the object-literal selector from earlier:

const data = useSelector(
  state => ({
    count: state.count,
    user: state.user
  })
)

Since the object is rebuilt on each call, the check sees:

same input
    ↓
different object
    ↓
unstable selector

Configuring how often the checks run

You can set the frequency for the whole app on the Provider:

<Provider
  store={store}
  stabilityCheck="always"
>
  <App />
</Provider>

or override it for an individual hook call:

const count = useSelector(
  selectCount,
  {
    devModeChecks: {
      stabilityCheck: 'once'
    }
  }
)

The accepted values are:

never
once
always

The default is 'once', meaning the check runs on the first call of each hook. None of this runs in production builds.

The identity function check

The second check looks for a selector that returns its input unchanged:

state => state

In a component, that looks like:

const state = useSelector(
  state => state
)

This ties the component to every store change:

Any Redux change
       ↓
Root state changes
       ↓
Component re-renders

The docs call this the identity function check. In earlier versions it went by the name noopCheck, which you may still see in older configuration.

The fix is the same as before: replace

const state = useSelector(
  state => state
)

with reads of the specific values you need:

const count = useSelector(
  state => state.counter.value
)

const user = useSelector(
  state => state.auth.currentUser
)

Rendering and performance beyond selectors

Parent renders still cascade

useSelector() only governs renders caused by store updates. It does nothing about the normal React rule that a component renders when its parent renders:

Parent renders
      ↓
Child renders

That happens even if no Redux state changed at all. When a child is expensive and its props are stable, wrap it in

React.memo()

This differs from connect(), whose wrapper behaves like a memoized component; hooks-based components get no such behavior for free.

Combining React.memo with useSelector

Here the component subscribes to a counter and is also memoized against its name prop:

const Counter = ({ name }) => {
  const count = useSelector(
    state => state.counter.value
  )

return (
    <div>
      {name}: {count}
    </div>
  )
}
export default React.memo(Counter)

The two mechanisms cover the two sources of renders:

Redux selector
       +
React.memo
       ↓
More controlled rendering

Memoization has its own cost, so profile first. For a broader catalog of render triggers, see our guide to common patterns that cause unnecessary React re-renders.

A performance model that fits on one screen

After each action, React-Redux re-runs the selectors of subscribed components, compares each result with the previous one, and renders only the components whose results differ:

Redux action
     ↓
Store updates
     ↓
Selectors execute
     ↓
Selector results compared
     ↓
Changed?
 ┌───┴────┐
No       Yes
│         │
│         ▼
│      Re-render
│
└── No Redux-triggered render

Your job, then, is to write selectors that:

  1. return only the data the component uses
  2. hand back stable references when nothing relevant changed
  3. do not allocate new objects or arrays without need
  4. memoize derived data that is expensive to compute

Rule one: never select the root state

useSelector(state => state)

Rule two: avoid building wrapper objects in selectors

A selector like this allocates on every run:

useSelector(state => ({
  user: state.user,
  count: state.count
}))

Only return an object like that when you deliberately pair it with

shallowEqual

or feed it through a memoized selector.

Rule three: memoize expensive derivations

Sorting, filtering, grouping and joining belong in

createSelector(...)

Rule four: apply React.memo on evidence

Before memoizing a component, walk through a short checklist:

Is the component expensive?
       ↓
Does it receive stable props?
       ↓
Does it re-render unnecessarily?
       ↓
Then consider React.memo()

If your codebase uses the React Compiler, much of this manual memoization may already be handled.

Rule five: select as narrowly as the component allows

Too broad:

state => state

Better:

state => state.auth.user

Better still, when the component displays nothing but the name:

state => state.auth.user.name

Rare edge cases: stale props and zombie children

Most applications never hit these two problems, but they explain why defensive selectors are a good habit.

Stale props

A stale-props problem needs a selector that depends on a prop, and a store update that changes both the state and, indirectly, that prop:

Selector depends on component props
        ↓
Redux action updates state
        ↓
Parent would receive new props
        ↓
Child selector runs first
        ↓
Selector sees old props

The child's subscription can fire before the parent has re-rendered and passed the new prop down. For a moment, the selector combines fresh state with an old prop. A typical shape is:

const todo = useSelector(
  state => state.todos[props.id]
)

If the item with that id was just removed, or the parent is about to pass a different id, the selector briefly reads data that no longer matches.

Writing selectors that tolerate missing data

The fragile version assumes the item always exists:

state.todos[props.id].name

A defensive version first looks the item up,

const todo =
  state.todos[props.id]

and only then reads from it:

return todo
  ? todo.name
  : undefined

The decision is a simple guard:

Does data exist?
      ↓
Yes → use it
No  → handle safely

Optional chaining (state.todos[id]?.name) expresses the same idea in one expression.

Zombie children

The zombie-child scenario involves a parent that renders a list and a child that subscribes to one item:

Parent
  │
  └── Child

The sequence goes like this:

  1. The child subscribes to the store.
  2. An action removes the data the child displays.
  3. The parent will stop rendering that child on its next render.
  4. Before that happens, the child's subscription runs.
  5. The child's selector reaches for data that is already gone.

An unguarded selector throws at that last step. React-Redux has mechanisms for selector errors caused by store updates, but defensive selectors remain the robust fix.

Why hooks are more exposed than connect

connect() builds a nested subscription tree: each connected component only updates after its connected ancestors have, which enforces top-down order. Hooks attach directly to the store without that hierarchy, so the ordering guarantees are weaker and these edge cases become theoretically possible.

Treat this as background knowledge, not a reason to avoid hooks. The documented position is that these issues are uncommon in real applications.

Advanced Provider features

A custom context for isolated stores

By default,

<Provider store={store}>

publishes the store through React-Redux's built-in context. A reusable component library that uses Redux internally can collide with the host application's store that way. To avoid that, Provider accepts a context of your own:

<Provider
  context={MyContext}
  store={myStore}
>

Hooks bound to that context come from factory functions:

createStoreHook()
createDispatchHook()
createSelectorHook()

This mainly matters for reusable libraries where several stores would otherwise collide.

batch() and React 18 automatic batching

Older code often wraps consecutive dispatches in batch() so React renders once instead of twice:

batch(() => {
  dispatch(action1())
  dispatch(action2())
})

React 18 batches updates automatically, including in promises and timeouts, so a typical React 18 app does not need batch() for this. Recent React-Redux releases keep it mostly for compatibility; check the current docs, and expect it in older code.

serverState for SSR and hydration

For server-side rendering, Provider takes an extra prop:

<Provider
  store={store}
  serverState={preloadedState}
>

The server renders HTML from an initial state and ships that state to the browser. serverState makes the hydration render use the same snapshot, preventing mismatches:

Server
  ↓
Initial Redux State
  ↓
HTML
  ↓
Browser Hydration
  ↓
Provider(serverState)
  ↓
Consistent initial render

A typical project layout

A TypeScript application built on Redux Toolkit is often organized by feature, with the store wiring in one place:

src/
│
├── app/
│   ├── store.ts
│   └── hooks.ts
│
├── features/
│   │
│   ├── counter/
│   │   ├── counterSlice.ts
│   │   └── Counter.tsx
│   │
│   ├── users/
│   │   ├── usersSlice.ts
│   │   └── Users.tsx
│   │
│   └── auth/
│       ├── authSlice.ts
│       └── Login.tsx
│
├── App.tsx
└── main.tsx

store.ts

The store module configures reducers and exports the inferred types:

const store = configureStore({
  reducer: {
    counter: counterReducer,
    users: usersReducer,
    auth: authReducer
  }
})

export type RootState =
  ReturnType<typeof store.getState>

export type AppDispatch =
  typeof store.dispatch

export type AppStore =
  typeof store

hooks.ts

The hooks module turns those types into application hooks:

export const useAppDispatch =
  useDispatch.withTypes<AppDispatch>()

export const useAppSelector =
  useSelector.withTypes<RootState>()

export const useAppStore =
  useStore.withTypes<AppStore>()

A component that uses both

A counter component then reads and writes through the typed hooks only:

function Counter() {
  const count = useAppSelector(
    state => state.counter.value
  )

const dispatch = useAppDispatch()
  return (
    <>
      <span>{count}</span>
      <button
        onClick={() =>
          dispatch(increment())
        }
      >
        +
      </button>
    </>
  )
}

The flow through this component is exactly the core loop from the start:

Component
    │
    ├── useAppSelector()
    │        ↓
    │      READ
    │
    └── useAppDispatch()
             ↓
          DISPATCH
             ↓
           Redux
             ↓
          New State
             ↓
       useAppSelector()
             ↓
          Component

Where Redux Toolkit fits

Redux Toolkit and React-Redux are complementary, not alternatives:

Redux Toolkit
      +
React-Redux

Redux Toolkit improves the Redux side: store setup, reducers, async logic, memoized selectors and data fetching.

configureStore
createSlice
createAsyncThunk
createSelector
RTK Query

React-Redux remains responsible for the connection to React:

Provider
useSelector
useDispatch
useStore
connect

The official React-Redux Quick Start sets up both together, and that combination is the default for new projects.

The complete flow in one diagram

Pulling every piece together, from the Provider down to the equality check:

React
                   │
                   ▼
             <Provider>
                   │
                   ▼
             Redux Store
                   │
          ┌────────┴────────┐
          │                 │
    useSelector()      useDispatch()
          │                 │
          │                 ▼
          │               Action
          │                 │
          │                 ▼
          │              Reducer
          │                 │
          │                 ▼
          │             New State
          │                 │
          └─────────┬───────┘
                    ▼
              Selector runs
                    │
                    ▼
             Equality check
                    │
             ┌──────┴──────┐
             │             │
           Same         Different
             │             │
             ▼             ▼
         No Redux       Re-render
          render

Common mistakes and how to spot them

Subscribing to the whole state

useSelector(state => state)

Replace it with specific selectors.

Wrapping values in a new object

useSelector(state => ({
  user: state.user
}))

Split it into separate hooks, or add shallowEqual deliberately.

Filtering or mapping inside the selector on every run

useSelector(state =>
  state.todos.filter(...)
)

If this runs often or on large lists, move it into a memoized selector.

Reading render data through useStore

Calling

store.getState()

in render logic gives you a value without a subscription. Use

useSelector()

so the component updates when the value changes.

Mutating state directly

An assignment like

state.user.name = 'John'

breaks Redux's immutable update model: references do not change, so selectors see "no change" and components do not render. (Inside Redux Toolkit's createSlice reducers, this style is allowed because Immer turns it into an immutable update; everywhere else it is a bug.)

Side effects inside selectors

state => {
  fetch(...)
  return state.user
}

A selector should compute and return, nothing else.

Memoizing by reflex

Sprinkling these everywhere

useMemo()
useCallback()
React.memo()

adds complexity and comparison overhead without proof that it helps. Optimize the render you have measured.

Misplacing memoized selector instances

A selector with a cache behaves differently depending on its scope. Before you use one, know whether it is:

global
per component
per component instance

A shared instance used with many different arguments may never hit its cache.

Interview questions with short answers

What is React-Redux, and why is Provider needed?

It is the official React binding for Redux: Provider, the hooks and connect let components read state, react to changes and dispatch actions. Provider puts the store into React context so any descendant can reach it.

useSelector versus useDispatch?

One reads:

useSelector
    ↓
READ Redux state

the other writes:

useDispatch
    ↓
DISPATCH Redux actions

How does useSelector trigger a re-render?

After each action it re-runs the selector and compares the result with the previous one using === or the equality function you passed. Only a difference schedules a render.

Why does this selector re-render constantly, and how do you fix it?

useSelector(state => ({
  user: state.user,
  count: state.count
}))

It builds a new object on every run, so the reference check always fails. The three remedies:

1. Multiple useSelector calls

2. shallowEqual

3. Memoized selector

useSelector versus connect?

Hooks compare selector results by reference; connect() shallow-compares the props from mapStateToProps. Hooks are the default, connect() stays supported.

Why memoized selectors?

They recompute derived data only when inputs change and otherwise return the same reference. A filter is the classic case:

todos
  ↓
filter completed
  ↓
new array

What are RootState, AppDispatch and withTypes()?

The inferred type of the whole state:

type RootState =
  ReturnType<typeof store.getState>

The inferred dispatch type, including middleware such as thunks:

type AppDispatch =
  typeof store.dispatch

And the helpers that return hooks pre-bound to those types:

useDispatch.withTypes<AppDispatch>()

useSelector.withTypes<RootState>()

useStore.withTypes<AppStore>()

Why typed hooks?

They replace repeated annotations like

useSelector(
  (state: RootState) =>
    state.user
)

with

useAppSelector(
  state => state.user
)

Why must selectors be pure?

A selector can run several times for the same state, during render, after every action, and at moments your code does not control. Any side effect would run an unpredictable number of times.

What is useStore for?

Uncommon cases that genuinely need the store object. Reading state for rendering goes through useSelector().

What are zombie children and stale props?

Both are rare ordering problems. A zombie child handles an update before its parent unmounts it and reads deleted data; stale props means a prop-dependent selector runs with fresh state but old props. Defensive selectors handle both.

Is batch() still needed with React 18?

Usually not, since React 18 batches automatically, but you will still meet it in older code.

Why can connect and hooks render differently?

Different subscription models and different comparisons:

connect()
    ↓
shallow equality

versus

useSelector()
    ↓
strict === equality

Why does useSelector run so often?

It:

  1. executes during render
  2. listens to the store
  3. re-executes after each dispatched action
  4. reuses its cached result during render only when the selector function and the state are unchanged

So an inline selector, being a new function each render, runs on every render; a stable selector reference lets React-Redux skip that call.

What to study, in order of priority

Level one: must know cold

Provider
useSelector
useDispatch
Redux Store flow
Selectors
=== equality
Re-render behavior
Redux Toolkit + React-Redux
TypeScript
RootState
AppDispatch
.withTypes()

Level two: solid working knowledge

shallowEqual
Memoized selectors
createSelector
useStore
connect
mapStateToProps
mapDispatchToProps
ConnectedProps
React.memo

Level three: advanced topics

Stale props
Zombie children
Selector + props
Selector memoization
Custom context
Development mode checks
SSR serverState
batch()

What not to memorize

You do not need to learn the documentation line by line. It is fine to skip:

  • how the subscription mechanism is implemented internally
  • rarely used connect() options
  • long-obsolete patterns
  • implementation details of the source code
  • the exact text of every development warning
  • every advanced Provider prop

What matters is the reasoning behind the rules. Knowing the fact

useSelector uses ===

is less useful than being able to answer

Why?

namely:

Because returning a new object
creates a new reference.

which leads to the chain

New reference
     ↓
=== false
     ↓
re-render

If you understand that chain, you can derive most of the other rules yourself.

Ten rules to keep

1. Read with useSelector

useSelector()

is how components read state.

2. Write with useDispatch

useDispatch()

is how components send actions.

3. Wrap the app in Provider

<Provider store={store}>

4. Remember the comparison rules

useSelector → ===
connect → shallow comparison

5. Never select the root state

useSelector(state => state)

6. Be careful with object-returning selectors

useSelector(state => ({
  ...
}))

7. Memoize expensive derived data

Memoized selector

8. Keep selectors disciplined

Pure
Predictable
Granular

9. Type the store, then the hooks

First the inferred types:

RootState
AppDispatch
AppStore

then the application hooks built from them:

useAppSelector
useAppDispatch
useAppStore

10. Use the modern pairing

Redux Toolkit
       +
React-Redux Hooks

The whole API on one page

A compact reference covering the core hooks, performance habits, TypeScript types, the legacy API and the advanced topics:

┌───────────────────────────────────────────────┐
│               REACT-REDUX                     │
├───────────────────────────────────────────────┤
│                                               │
│ <Provider store={store}>                      │
│        ↓                                      │
│ Makes Redux available to React                │
│                                               │
│ useSelector()                                 │
│        ↓                                      │
│ READ state                                    │
│        ↓                                      │
│ Default comparison: ===                       │
│                                               │
│ useDispatch()                                 │
│        ↓                                      │
│ DISPATCH actions                              │
│                                               │
│ useStore()                                    │
│        ↓                                      │
│ Direct store access                           │
│        ↓                                      │
│ Use rarely                                    │
│                                               │
├───────────────────────────────────────────────┤
│ PERFORMANCE                                   │
├───────────────────────────────────────────────┤
│                                               │
│ Avoid state => state                          │
│ Avoid unnecessary object creation             │
│ Use granular selectors                        │
│ Use shallowEqual when appropriate             │
│ Use memoized selectors for derived data       │
│ Use React.memo when justified                 │
│                                               │
├───────────────────────────────────────────────┤
│ TYPESCRIPT                                    │
├───────────────────────────────────────────────┤
│                                               │
│ RootState = ReturnType<typeof store.getState> │
│ AppDispatch = typeof store.dispatch           │
│ AppStore = typeof store                       │
│                                               │
│ useAppSelector                                │
│ useAppDispatch                                │
│ useAppStore                                   │
│                                               │
├───────────────────────────────────────────────┤
│ LEGACY / EXISTING APPLICATIONS                │
├───────────────────────────────────────────────┤
│                                               │
│ connect()                                     │
│ mapStateToProps                               │
│ mapDispatchToProps                            │
│ ConnectedProps                                │
│                                               │
├───────────────────────────────────────────────┤
│ ADVANCED                                      │
├───────────────────────────────────────────────┤
│                                               │
│ Stale Props                                   │
│ Zombie Children                               │
│ Custom Context                                │
│ SSR serverState                               │
│ Development checks                            │
│ batch()                                       │
│                                               │
└───────────────────────────────────────────────┘

And the cycle, drawn once more with the read and write paths side by side:

                        REACT
                           │
                           │
                    <Provider />
                           │
                           ▼
                    ┌─────────────┐
                    │ Redux Store │
                    └──────┬──────┘
                           │
               ┌───────────┴───────────┐
               │                       │
               ▼                       ▲
        useSelector()             useDispatch()
               │                       │
               │                       │
             READ                    ACTION
               │                       │
               │                       │
               │                  ┌────┴─────┐
               │                  │ Reducer  │
               │                  └────┬─────┘
               │                       │
               │                       ▼
               │                  New State
               │                       │
               └───────────────────────┘
                           │
                           ▼
                    Equality Check
                           │
                    ┌──────┴──────┐
                    │             │
                  Same         Changed
                    │             │
                    ▼             ▼
                No Redux      Re-render
                 render

For a new TypeScript project, the recommended setup reduces to this shape:

            Redux Toolkit
                   +
              React-Redux
                   │
        ┌──────────┴──────────┐
        │                     │
     Store                 Provider
        │                     │
        │               Application tree
        │                     │
        └──────────┬──────────┘
                   │
          ┌────────┴────────┐
          │                 │
   useAppSelector()   useAppDispatch()
          │                 │
        READ              WRITE
          │                 │
          └────────┬────────┘
                   │
                Redux
                   │
              New State
                   │
                Selector
                   │
                Re-render

Wrapping up

React-Redux gets much easier once you stop treating its exports as unrelated tools. This list of names

Provider
useSelector
useDispatch
connect
shallowEqual
createSelector
useStore

describes a single pipeline with one job per stage. Provider exposes the store:

Provider
   ↓
makes Store available

The selector hook reads:

useSelector
   ↓
reads selected state

The dispatch hook sends:

useDispatch
   ↓
sends actions

Reducers produce the next state:

Reducer
   ↓
creates new state

Selectors shape it for the UI:

Selector
   ↓
derives data

The equality check decides whether anything relevant changed:

Equality
   ↓
decides whether selected data changed

And React does the rendering:

React
   ↓
re-renders when necessary

On the TypeScript side, the chain is equally linear:

Store
 ↓
RootState
AppDispatch
AppStore
 ↓
.withTypes()
 ↓
useAppSelector()
useAppDispatch()
useAppStore()

When a component renders more than it should, walk through the same four questions every time:

useSelector()
      ↓
What does my selector return?
      ↓
Is the reference stable?
      ↓
Does the selected value actually change?
      ↓
Should this component re-render?

Key takeaways:

  • Most React-Redux performance issues come from selectors that return new references, not from Redux itself.
  • useSelector() compares with ===; connect() shallow-compares. Porting code between them without adjusting selectors changes behavior.
  • Select narrowly, memoize derived data with module-level createSelector instances, and use shallowEqual only when an object result is deliberate.
  • Infer RootState, AppDispatch and AppStore from the store and build typed hooks with .withTypes().
  • Stale props, zombie children, custom contexts, batch() and serverState are worth understanding, but they are edge cases, not everyday concerns.

As a self-test, try explaining every heading of this guide from memory, from Provider and equality through typed hooks to serverState, and writing the basic setup without looking anything up.

For the details, the official references are the hooks API, the Provider API, the TypeScript usage guide, the Quick Start and the connect() reference. Study the core loop and equality first, then selectors, TypeScript, performance, connect and the edge cases, so the API details have a model to attach to.