| |

React 68 ⚛️ Dispatching Actions and Selecting State

⏱️ Estimated read: 15 min | |

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

FormPurpose
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

FormPurpose
(state) => state.sliceThe basic
(state, props) => ...The prop-based
shallowEqualThe shallow compare
createSelectorThe memoized
createSelector + useSelectorThe 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

PatternThe effect
The narrow selectorThe re-render only on the value change
The multiple selectorsThe independent subscription
The shallowEqualThe shallow compare
The createSelectorThe memoized derived
The stable dispatchThe 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

FormPurpose
dispatch(action)Send the action
dispatch(thunk())Run the thunk
.unwrap()The promise
useAppDispatchThe typed

The Selector

FormPurpose
useSelector((state) => ...)The read
useAppSelectorThe typed
The second argumentThe equality function
shallowEqualThe shallow compare
createSelectorThe memoized

The Equality Functions

FunctionCompare
Object.is (the default)The reference
shallowEqualThe top-level properties
The customThe custom predicate

The Performance Patterns

PatternThe effect
The narrow selectorThe minimal re-render
The multiple selectorsThe independent
The shallowEqualThe shallow compare
The createSelectorThe memoized derived
The stable dispatchThe effect runs once

The Selector Examples

SelectorPurpose
(state) => state.todos.itemsThe slice
(state) => state.todos.items.lengthThe 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

PitfallWhy It HappensFix
The re-render loopThe new object in the selectorUse shallowEqual
The component re-renders too oftenThe broad selectorNarrow the selector
The derived value recomputesNo createSelectorAdd the memoization
The effect runs twiceThe StrictModeThe expected in the development
The dispatch unknownThe untypedUse the typed dispatch
The async error unhandledNo .unwrap()Add the try/catch
The stale selectorThe prop closureUse 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

ItemValue
useDispatchThe dispatch function
useAppDispatchThe typed dispatch
useSelectorThe read the state
useAppSelectorThe typed selector
shallowEqualThe shallow compare
createSelectorThe memoized derived
.unwrap()The promise from the thunk
The narrow selectorThe minimal re-render
The multiple selectorsThe independent
The stable dispatchThe effect runs once

Key takeaways:

  • The useDispatch and the useSelector are the entire interface. The dispatch writes, and the selector reads. The component does not touch the store directly.
  • The useSelector re-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 shallowEqual or the separate selectors fix it.
  • The createSelector memoizes 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 useAppDispatch and the useAppSelector are the typed from the AppDispatch and the RootState.
  • The .unwrap() returns the promise from the thunk. The try/catch handles the rejection, and the resolved value is the payload.
  • The dispatch in 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!