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.
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:
- reads the current store state
- calls your selector with it
- keeps the returned value as the "last selected" result
- registers a subscription to the store for this component
- re-runs the selector after each dispatched action
- compares the previous result to the new one
- 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
dispatchaccepts - 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:
- return only the data the component uses
- hand back stable references when nothing relevant changed
- do not allocate new objects or arrays without need
- 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:
- The child subscribes to the store.
- An action removes the data the child displays.
- The parent will stop rendering that child on its next render.
- Before that happens, the child's subscription runs.
- 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:
- executes during render
- listens to the store
- re-executes after each dispatched action
- 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
createSelectorinstances, and useshallowEqualonly when an object result is deliberate. - Infer
RootState,AppDispatchandAppStorefrom the store and build typed hooks with.withTypes(). - Stale props, zombie children, custom contexts,
batch()andserverStateare 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.