React 68 ⚛️ Dispatching Actions and Selecting State
A Redux component does two things: it reads the state and it changes the state. The read is the useSelector, and the write is the useDispatch. The two hooks are the entire interface between the component and the store, and the way they are used determines whether the application is fast, predictable, and easy to test. The dispatch sends the action to the reducer, and the selector reads the slice from the store. The component re-renders when the selected value changes, and the selection determines the re-render frequency.
Key point: The useSelector compares the selected value with the previous one using the Object.is. If the value is a new object reference on every call, the component re-renders on every store change. The selector must return the stable reference or the primitive, and the createSelector from the Reselect memoizes the derived values.
Why the dispatch and the selector matter
The interface problem. The component does not read the store directly. The useSelector subscribes to the store, and the useDispatch returns the dispatch. The two hooks are the only API the component needs.
The re-render problem. The useSelector runs the selector after every dispatched action. If the returned value is different from the previous, the component re-renders. The selector that returns the new object every time produces the re-render on every action, regardless of whether the slice changed.
The batching problem. The React 18 batches the state updates, including the Redux dispatches. The two dispatches in the same event handler produce one re-render. The unstable_batchedUpdates is the manual for the older React.
The reference problem. The selector that returns the derived object should use the createSelector. The memoized selector returns the same reference when the inputs have not changed, and the component skips the re-render.
The strict-mode problem. The React StrictMode runs the reducer twice in the development. The reducer must be the pure, and the double-invocation is the check.
The dispatch problem. The dispatch returns the action. The thunk returns the promise. The unwrap() on the thunk result is the pattern for the error handling.
The test problem. The component is tested with the renderWithProviders that wraps the Provider and the store. The test dispatches the action and asserts the selector.
a. The dispatch
The useDispatch returns the dispatch function. The dispatch sends the action to the store, and the reducer computes the new state.
The basic dispatch
import { useDispatch } from 'react-redux';
import { addTodo } from './todosSlice';
function AddTodoForm() {
const dispatch = useDispatch();
function handleSubmit(e) {
e.preventDefault();
const text = e.target.elements.text.value;
dispatch(addTodo(text));
e.target.reset();
}
return (
<form onSubmit={handleSubmit}>
<input name="text" />
<button type="submit">Add</button>
</form>
);
}
The dispatch(addTodo(text)) sends the action, and the reducer updates the todos slice.
The typed dispatch
// app/hooks.js
import { useDispatch } from 'react-redux';
export const useAppDispatch = () => useDispatch();
// app/hooks.ts
import { useDispatch } from 'react-redux';
import type { AppDispatch } from './store';
export const useAppDispatch = () => useDispatch<AppDispatch>();
The typed dispatch carries the thunk type, so the dispatch(fetchTodos()) is typed.
The thunk dispatch
import { fetchTodos } from './todosSlice';
function TodoList() {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(fetchTodos());
}, [dispatch]);
// ...
}
The fetchTodos() is the thunk, and the dispatch runs it.
The action payload
dispatch(addTodo('Learn Redux'));
// { type: 'todos/addTodo', payload: 'Learn Redux' }
The action creator produces the action object, and the dispatch sends it.
The unwrap
async function handleAdd(text) {
try {
const result = await dispatch(addTodoAsync(text)).unwrap();
console.log('Added:', result);
} catch (error) {
console.error('Failed:', error);
}
}
The .unwrap() returns the promise that resolves to the payload or rejects with the error.
The conditional dispatch
function handleSubmit() {
if (!text) return;
dispatch(addTodo(text));
}
The dispatch is the conditional, and the action is sent only when the condition holds.
The multiple dispatches
function handleComplete() {
dispatch(completeTodo(id));
dispatch(logAction('completed', id));
}
The two dispatches in the same handler are batched into the one re-render.
The dispatch table
| Form | Purpose |
|---|---|
dispatch(action) | Send the action |
dispatch(thunk()) | Run the thunk |
dispatch(thunk()).unwrap() | The promise |
dispatch(condition ? a : b) | The conditional |
b. The selector
The useSelector takes the selector function and returns the selected value. The component re-renders when the value changes.
The basic selector
import { useSelector } from 'react-redux';
function TodoList() {
const todos = useSelector((state) => state.todos.items);
return <ul>{todos.map((t) => <li key={t.id}>{t.text}</li>)}</ul>;
}
The selector reads the state.todos.items, and the component re-renders when the items reference changes.
The typed selector
// app/hooks.js
import { useSelector } from 'react-redux';
export const useAppSelector = useSelector;
// app/hooks.ts
import { useSelector, TypedUseSelectorHook } from 'react-redux';
import type { RootState } from './store';
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
The typed selector infers the state type from the RootState.
The multiple selectors
function TodoHeader() {
const count = useAppSelector((state) => state.todos.items.length);
const filter = useAppSelector((state) => state.todos.filter);
return <div>{count} todos, filter: {filter}</div>;
}
The two selectors read the two values. Each selector is independent, and the component re-renders when either changes.
The selector with the props
function TodoItem({ id }) {
const todo = useAppSelector((state) => state.todos.items.find((t) => t.id === id));
return <li>{todo?.text}</li>;
}
The selector uses the prop. The selector runs after every dispatch, and the find is the O(n) per item.
The useSelector and the equality
The useSelector uses the Object.is by default. The custom equality is the second argument.
const todos = useSelector(
(state) => state.todos.items,
(left, right) => left.length === right.length
);
The custom equality compares the length, and the component re-renders only when the length changes.
The shallowEqual
import { shallowEqual, useSelector } from 'react-redux';
const { items, filter } = useSelector(
(state) => ({ items: state.todos.items, filter: state.todos.filter }),
shallowEqual
);
The shallowEqual compares the top-level properties. The component re-renders only when one of the properties changes.
The createSelector
import { createSelector } from '@reduxjs/toolkit';
const selectTodos = (state) => state.todos.items;
const selectFilter = (state) => state.todos.filter;
export const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'completed': return todos.filter((t) => t.completed);
case 'active': return todos.filter((t) => !t.completed);
default: return todos;
}
}
);
The createSelector memoizes the result. The selector re-computes only when the inputs change, and the returned reference is stable otherwise.
The component with the memoized selector
function FilteredTodoList() {
const filtered = useAppSelector(selectFilteredTodos);
return <ul>{filtered.map((t) => <li key={t.id}>{t.text}</li>)}</ul>;
}
The component re-renders only when the filtered list changes.
The selector table
| Form | Purpose |
|---|---|
(state) => state.slice | The basic |
(state, props) => ... | The prop-based |
shallowEqual | The shallow compare |
createSelector | The memoized |
createSelector + useSelector | The derived |
c. The performance patterns
The selector and the dispatch have the performance implications. The patterns below are the ones that keep the component fast.
The narrow selector
// ❌ The broad selector re-renders on any change.
const state = useAppSelector((state) => state);
// ✅ The narrow selector re-renders only when the slice changes.
const todos = useAppSelector((state) => state.todos.items);
The narrow selector returns the specific value, and the component re-renders only when the value changes.
The multiple useSelector calls
// ✅ The two selectors, each independent.
const count = useAppSelector((state) => state.todos.items.length);
const filter = useAppSelector((state) => state.todos.filter);
The React-Redux runs the selectors after the dispatch, and the component re-renders when any of the selected values change.
The object selector
// ❌ The new object every call.
const { items, filter } = useAppSelector((state) => ({
items: state.todos.items,
filter: state.todos.filter,
}));
// ✅ The shallowEqual.
const { items, filter } = useAppSelector(
(state) => ({ items: state.todos.items, filter: state.todos.filter }),
shallowEqual
);
// ✅ The separate selectors.
const items = useAppSelector((state) => state.todos.items);
const filter = useAppSelector((state) => state.todos.filter);
The object selector returns the new reference on every call, and the component re-renders on every action. The shallowEqual or the separate selectors fix it.
The memoized derived value
// ❌ The derived value recomputed on every render.
const completedCount = useAppSelector((state) =>
state.todos.items.filter((t) => t.completed).length
);
// ✅ The memoized selector.
const selectCompletedCount = createSelector(
[(state) => state.todos.items],
(items) => items.filter((t) => t.completed).length
);
const completedCount = useAppSelector(selectCompletedCount);
The memoized selector recomputes only when the items change.
The dispatch in the effect
useEffect(() => {
dispatch(fetchTodos());
}, [dispatch]);
The dispatch is stable, and the effect runs once. The missing dependency produces the warning.
The performance table
| Pattern | The effect |
|---|---|
| The narrow selector | The re-render only on the value change |
| The multiple selectors | The independent subscription |
The shallowEqual | The shallow compare |
The createSelector | The memoized derived |
| The stable dispatch | The effect runs once |
Complete Example Session
// ============================================
// PART 1: THE BASIC DISPATCH
// ============================================
import { useDispatch } from 'react-redux';
import { addTodo } from './todosSlice';
function AddTodo() {
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(addTodo('New todo'))}>
Add
</button>
);
}
// ============================================
// PART 2: THE TYPED DISPATCH
// ============================================
import { useAppDispatch } from '../app/hooks';
function AddTodo() {
const dispatch = useAppDispatch();
return <button onClick={() => dispatch(addTodo('New'))}>Add</button>;
}
// ============================================
// PART 3: THE BASIC SELECTOR
// ============================================
import { useSelector } from 'react-redux';
function TodoList() {
const todos = useSelector((state) => state.todos.items);
return <ul>{todos.map((t) => <li key={t.id}>{t.text}</li>)}</ul>;
}
// ============================================
// PART 4: THE TYPED SELECTOR
// ============================================
import { useAppSelector } from '../app/hooks';
function TodoList() {
const todos = useAppSelector((state) => state.todos.items);
return <ul>{todos.map((t) => <li key={t.id}>{t.text}</li>)}</ul>;
}
// ============================================
// PART 5: THE MULTIPLE SELECTORS
// ============================================
function TodoHeader() {
const count = useAppSelector((state) => state.todos.items.length);
const filter = useAppSelector((state) => state.todos.filter);
return <div>{count} todos ({filter})</div>;
}
// ============================================
// PART 6: THE SHALLOW EQUAL
// ============================================
import { shallowEqual } from 'react-redux';
function TodoSummary() {
const { count, filter } = useAppSelector(
(state) => ({
count: state.todos.items.length,
filter: state.todos.filter,
}),
shallowEqual
);
return <div>{count} todos ({filter})</div>;
}
// ============================================
// PART 7: THE CREATE SELECTOR
// ============================================
import { createSelector } from '@reduxjs/toolkit';
const selectTodos = (state) => state.todos.items;
const selectFilter = (state) => state.todos.filter;
export const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'completed': return todos.filter((t) => t.completed);
case 'active': return todos.filter((t) => !t.completed);
default: return todos;
}
}
);
// ============================================
// PART 8: THE MEMOIZED COMPONENT
// ============================================
function FilteredTodoList() {
const filtered = useAppSelector(selectFilteredTodos);
return <ul>{filtered.map((t) => <li key={t.id}>{t.text}</li>)}</ul>;
}
// ============================================
// PART 9: THE THUNK DISPATCH
// ============================================
function TodoLoader() {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(fetchTodos());
}, [dispatch]);
return null;
}
// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================
// 1. Use the useDispatch for the write.
// 2. Use the useAppDispatch for the TypeScript.
// 3. Use the useSelector for the read.
// 4. Use the useAppSelector for the TypeScript.
// 5. Use the narrow selector.
// 6. Use the multiple selectors for the independent values.
// 7. Use the shallowEqual for the object selector.
// 8. Use the createSelector for the derived values.
// 9. Use the .unwrap() for the async error handling.
// 10. Use the dispatch in the effect with the dispatch dependency.
The ten parts showed the basic dispatch, the typed dispatch, the basic selector, the typed selector, the multiple selectors, the shallowEqual, the createSelector, the memoized component, the thunk dispatch, and the complete pattern.
Quick Reference
The Dispatch
| Form | Purpose |
|---|---|
dispatch(action) | Send the action |
dispatch(thunk()) | Run the thunk |
.unwrap() | The promise |
useAppDispatch | The typed |
The Selector
| Form | Purpose |
|---|---|
useSelector((state) => ...) | The read |
useAppSelector | The typed |
| The second argument | The equality function |
shallowEqual | The shallow compare |
createSelector | The memoized |
The Equality Functions
| Function | Compare |
|---|---|
Object.is (the default) | The reference |
shallowEqual | The top-level properties |
| The custom | The custom predicate |
The Performance Patterns
| Pattern | The effect |
|---|---|
| The narrow selector | The minimal re-render |
| The multiple selectors | The independent |
The shallowEqual | The shallow compare |
The createSelector | The memoized derived |
| The stable dispatch | The effect runs once |
The Selector Examples
| Selector | Purpose |
|---|---|
(state) => state.todos.items | The slice |
(state) => state.todos.items.length | The count |
(state, id) => ...find(...) | The prop-based |
createSelector(...) | The derived |
Best Practices
✅ Do This:
// Use the narrow selector.
const todos = useAppSelector((state) => state.todos.items); // ✅
// Use the multiple selectors for the independent values.
const count = useAppSelector((state) => state.todos.items.length);
const filter = useAppSelector((state) => state.todos.filter); // ✅
// Use the shallowEqual for the object selector.
const { items, filter } = useAppSelector((state) => ({ ... }), shallowEqual); // ✅
// Use the createSelector for the derived values.
const selectFiltered = createSelector([selectTodos, selectFilter], ...); // ✅
// Use the dispatch in the effect with the dependency.
useEffect(() => { dispatch(fetchTodos()); }, [dispatch]); // ✅
// Use the .unwrap() for the async.
const result = await dispatch(addTodoAsync(text)).unwrap(); // ✅
// Use the typed hooks.
const dispatch = useAppDispatch();
const todos = useAppSelector((state) => state.todos.items); // ✅
❌ Don’t Do This:
// Don't select the whole state.
const state = useAppSelector((state) => state); // ❌ The re-render on any change
// Don't return the new object without the equality.
const { items } = useAppSelector((state) => ({ items: state.todos.items })); // ❌ The new reference
// Don't recompute the derived value in the selector.
const filtered = useAppSelector((state) =>
state.todos.items.filter((t) => t.completed)
); // ❌ The new array every call
// Don't forget the dispatch dependency.
useEffect(() => { dispatch(fetchTodos()); }, []); // ⚠️ The lint warning
// Don't use the untyped dispatch in the TypeScript.
const dispatch = useDispatch(); // ⚠️ The unknown action
// Don't dispatch in the render.
function Component() {
dispatch(addTodo('text')); // ❌ The render loop
return null;
}
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The re-render loop | The new object in the selector | Use shallowEqual |
| The component re-renders too often | The broad selector | Narrow the selector |
| The derived value recomputes | No createSelector | Add the memoization |
| The effect runs twice | The StrictMode | The expected in the development |
| The dispatch unknown | The untyped | Use the typed dispatch |
| The async error unhandled | No .unwrap() | Add the try/catch |
| The stale selector | The prop closure | Use the useSelector with the prop |
Real-World Examples
1. The Basic Dispatch
dispatch(addTodo('New todo'));
2. The Typed Dispatch
const dispatch = useAppDispatch();
3. The Basic Selector
const todos = useAppSelector((state) => state.todos.items);
4. The Multiple Selectors
const count = useAppSelector((state) => state.todos.items.length);
const filter = useAppSelector((state) => state.todos.filter);
5. The ShallowEqual
const { items, filter } = useAppSelector((state) => ({ ... }), shallowEqual);
6. The CreateSelector
export const selectFilteredTodos = createSelector([selectTodos, selectFilter], ...);
7. The Thunk Dispatch
useEffect(() => { dispatch(fetchTodos()); }, [dispatch]);
8. The Unwrap
const result = await dispatch(addTodoAsync(text)).unwrap();
9. The Memoized Component
const filtered = useAppSelector(selectFilteredTodos);
10. The Complete Pattern
const dispatch = useAppDispatch();
const todos = useAppSelector((state) => state.todos.items);
Visual
The Dispatch Flow
┌────────────────────────────────────────────────────────────┐
│ THE DISPATCH FLOW │
│ │
│ ┌──────────────┐ │
│ │ Component │ │
│ │ handleClick │ │
│ └──────┬───────┘ │
│ │ │
│ v │
│ ┌──────────────┐ │
│ │ dispatch │ │
│ │ (addTodo) │ │
│ └──────┬───────┘ │
│ │ │
│ v │
│ ┌──────────────┐ │
│ │ Reducer │ │
│ │ (the slice) │ │
│ └──────┬───────┘ │
│ │ │
│ v │
│ ┌──────────────┐ │
│ │ Store │ │
│ │ (the new │ │
│ │ state) │ │
│ └──────┬───────┘ │
│ │ │
│ v │
│ ┌──────────────┐ │
│ │ useSelector │ │
│ │ (re-run) │ │
│ └──────┬───────┘ │
│ │ │
│ v │
│ ┌──────────────┐ │
│ │ Re-render │ │
│ │ (if the │ │
│ │ value │ │
│ │ changed) │ │
│ └──────────────┘ │
└────────────────────────────────────────────────────────────┘
The Selector
┌────────────────────────────────────────────────────────────┐
│ THE SELECTOR │
│ │
│ const todos = useAppSelector((state) => state.todos.items);│
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The selector runs after every dispatch. │ │
│ │ The result is compared with the previous. │ │
│ │ If the different, the component re-renders. │ │
│ │ │ │
│ │ The narrow selector → the less re-render. │ │
│ │ The broad selector → the more re-render. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The createSelector
┌────────────────────────────────────────────────────────────┐
│ THE createSelector │
│ │
│ const selectFiltered = createSelector( │
│ [selectTodos, selectFilter], │
│ (todos, filter) => todos.filter((t) => ...) │
│ ); │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ The inputs: the todos and the filter. │ │
│ │ The result: the filtered list. │ │
│ │ │ │
│ │ The memoization: │ │
│ │ The same inputs → the same reference. │ │
│ │ The different inputs → the new result. │ │
│ │ │ │
│ │ The component re-renders only when the │ │
│ │ filtered list changes. │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Performance
┌────────────────────────────────────────────────────────────┐
│ THE PERFORMANCE │
│ │
│ ❌ THE BROAD SELECTOR │
│ ┌──────────────────────────────────────────────────┐ │
│ │ const state = useAppSelector((state) => state); │ │
│ │ → The component re-renders on EVERY action. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ✅ THE NARROW SELECTOR │
│ ┌──────────────────────────────────────────────────┐ │
│ │ const todos = useAppSelector((state) => │ │
│ │ state.todos.items); │ │
│ │ → The component re-renders only when the │ │
│ │ items change. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ The selector is the subscription, and the narrow is the │
│ less re-render. │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
useDispatch | The dispatch function |
useAppDispatch | The typed dispatch |
useSelector | The read the state |
useAppSelector | The typed selector |
shallowEqual | The shallow compare |
createSelector | The memoized derived |
.unwrap() | The promise from the thunk |
| The narrow selector | The minimal re-render |
| The multiple selectors | The independent |
| The stable dispatch | The effect runs once |
Key takeaways:
- The
useDispatchand theuseSelectorare the entire interface. The dispatch writes, and the selector reads. The component does not touch the store directly. - The
useSelectorre-runs after every dispatch, and the component re-renders when the selected value changes. The narrow selector reduces the re-render, and the broad selector increases it. - The object selector without the equality returns the new reference on every call. The component re-renders on every action. The
shallowEqualor the separate selectors fix it. - The
createSelectormemoizes the derived value. The result is recomputed only when the inputs change. The returned reference is stable, and the component skips the re-render. - The typed hooks are the TypeScript convention. The
useAppDispatchand theuseAppSelectorare the typed from theAppDispatchand theRootState. - The
.unwrap()returns the promise from the thunk. Thetry/catchhandles the rejection, and the resolved value is the payload. - The
dispatchin the effect must be in the dependency array. The dispatch is stable, and the effect runs once. The missing dependency produces the warning. - The React 18 batches the dispatches. The two dispatches in the same handler produce the one re-render.
Remember: The dispatching and the selecting are the two halves of the Redux component interface. The useDispatch sends the action, and the useSelector reads the slice. The narrow selector and the memoized derived value keep the component fast, and the typed hooks keep the TypeScript. The result is the component that reads the state it needs and writes the state it changes, and the re-render that happens only when the selected value changes.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!