Skip to main content

🎣 useSelector and useDispatch Hooks

The two React-Redux hooks look deceptively simple β€” one reads state, the other sends actions. But the details around how they re-render, when a selector recomputes, and which equality check applies are exactly what separates a snappy Redux app from a sluggish one. This lesson takes both hooks apart and puts them back together with real patterns.

Week 6 · Day 2 (Tuesday: React-Redux Integration) · Lecture 2

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Write selector functions that return exactly the state a component needs
  • Explain how useSelector decides whether to re-render, and change that with an equality function
  • Build memoized selectors with createSelector and parameterized/factory selectors for per-item data
  • Dispatch synchronous and async (thunk) actions with useDispatch
  • Know why dispatch is stable and how that affects useEffect/useCallback
  • Package Redux logic into reusable custom hooks

Estimated Time: 75 minutes

Practice: Fix a re-render bug, then build a useCart custom hook backed by memoized selectors.

In This Lesson

Two Hooks, One Loop

Every interaction in a React-Redux app is the same round trip. A component reads state with useSelector to render. Something happens β€” a click, a fetch β€” and the component dispatches an action with useDispatch. The reducer produces new state, the store notifies subscribers, and any component whose selected value changed re-renders with fresh data.

graph TD A["Component renders"] --> B["useSelector reads a slice"] B --> C["User interaction"] C --> D["useDispatch sends an action"] D --> E["Reducer returns new state"] E --> F["Store notifies subscribers"] F --> G{"Selected value changed?"} G -->|Yes| A G -->|No| H["No re-render"]

🎣 The fishing analogy

Think of the store as an ocean of state. useSelector is your fishing line β€” the selector function is the lure that decides exactly which fish (which slice) you pull up. useDispatch is your cast back into the water, sending an action that changes what's swimming down there. And memoization is a smart net that refuses to haul up the same catch twice, so your component only reacts when something genuinely new appears.

useSelector In-Depth

A selector is just a function (state) => value. The cleanest ones read a single slice; the value you return is what the component gets and what re-render decisions are based on.

Basic and multiple selects

import { useSelector } from 'react-redux';

function UserProfile() {
  const user = useSelector((state) => state.user);
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

function Dashboard() {
  // Prefer several narrow selectors over one big object.
  const name          = useSelector((state) => state.user.name);
  const notifications = useSelector((state) => state.notifications);
  const isLoading     = useSelector((state) => state.ui.isLoading);

  if (isLoading) return <p>Loading…</p>;
  return (
    <div>
      <h1>Welcome, {name}</h1>
      <NotificationList items={notifications} />
    </div>
  );
}

Computed values inside a selector

A selector can derive a value rather than return a raw slice. When the derived value is a primitive (a number, string, boolean), reference equality just works β€” the component re-renders only when the number itself changes.

function CartCount() {
  // Returns a number β†’ safe to compute inline; === compares by value.
  const totalItems = useSelector((state) =>
    state.cart.items.reduce((sum, item) => sum + item.quantity, 0)
  );
  return <span>{totalItems} items</span>;
}

Selecting with a prop

A component often needs one item out of a collection, keyed by a prop. Read the whole collection in the selector's closure and pick the item you need:

function TodoItem({ todoId }) {
  const todo = useSelector((state) =>
    state.todos.find((t) => t.id === todoId)
  );
  if (!todo) return null;
  return (
    <label>
      <input type="checkbox" checked={todo.completed} readOnly />
      {todo.text}
    </label>
  );
}

Re-render & Equality Rules

Here's the single most important thing to internalize: after every dispatched action, React-Redux runs your selector and compares the new result to the previous one. By default that comparison is === (reference equality). If the result is === to last time, the component does not re-render.

useSelector re-render decision: primitives and stable refs skip re-render, new object literals force one state.value (a number) β†’ 42 === 42 prev === next βœ” skip re-render primitives & unchanged slices compare cheaply and correctly { a, b } β†’ a brand-new object each render prev !== next ✘ re-render every single time fix with separate selects, shallowEqual, or createSelector
Reference equality is fast and correct for primitives and stable references, but a freshly built object or array defeats it.

The classic mistake β€” and three fixes

// ❌ New object literal every render β†’ always !== β†’ re-renders constantly
const data = useSelector((state) => ({
  user:  state.user,
  posts: state.posts,
}));

// βœ… Fix 1 β€” separate selectors (each returns a stable slice)
const user  = useSelector((state) => state.user);
const posts = useSelector((state) => state.posts);

// βœ… Fix 2 β€” shallowEqual compares the object field-by-field
import { shallowEqual } from 'react-redux';
const data2 = useSelector(
  (state) => ({ user: state.user, posts: state.posts }),
  shallowEqual
);

// βœ… Fix 3 β€” memoize with createSelector (see next section)

πŸ’‘ Picking an equality strategy

Return a primitive when you can β€” it sidesteps the whole problem. Need several fields at once? Use shallowEqual. Computing a filtered/sorted collection? Reach for a memoized selector so the same input yields the exact same output reference.

Memoized & Parameterized Selectors

When a selector derives data β€” filtering, mapping, sorting, summing β€” recomputing on every store change wastes work and, worse, produces a new array/object reference each time (defeating ===). Reselect's createSelector (re-exported by Redux Toolkit) caches the last result and only recomputes when its input selectors return something new.

A memoized summary selector

import { createSelector } from '@reduxjs/toolkit';

// Input selectors β€” cheap, just read slices.
const selectItems   = (state) => state.cart.items;
const selectTaxRate = (state) => state.config.taxRate;

// Result function only runs when items OR taxRate changes.
// Otherwise the SAME { subtotal, tax, total } object is returned.
const selectCartSummary = createSelector(
  [selectItems, selectTaxRate],
  (items, taxRate) => {
    const subtotal = items.reduce((s, i) => s + i.price * i.quantity, 0);
    const tax = subtotal * taxRate;
    return { subtotal, tax, total: subtotal + tax };
  }
);

function CartSummary() {
  const { subtotal, tax, total } = useSelector(selectCartSummary);
  return (
    <div>
      <p>Subtotal: ${subtotal.toFixed(2)}</p>
      <p>Tax: ${tax.toFixed(2)}</p>
      <strong>Total: ${total.toFixed(2)}</strong>
    </div>
  );
}

Parameterized selectors (passing an argument)

A memoized selector can take a second argument β€” useful for "give me the todos matching this filter":

const selectFilteredTodos = createSelector(
  [(state) => state.todos, (state, filter) => filter],
  (todos, filter) => {
    switch (filter) {
      case 'completed': return todos.filter((t) => t.completed);
      case 'active':    return todos.filter((t) => !t.completed);
      default:          return todos;
    }
  }
);

function TodoList({ filter }) {
  const todos = useSelector((state) => selectFilteredTodos(state, filter));
  return <ul>{todos.map((t) => <li key={t.id}>{t.text}</li>)}</ul>;
}

⚠️ One selector instance can only cache one argument

A single createSelector caches the most recent inputs. If two <TodoList>s render at once with different filter values, they thrash the cache. The fix is a factory: a function that makes a fresh memoized selector per component instance.

import { useMemo } from 'react';

const makeSelectItemById = () => createSelector(
  [(state) => state.items, (state, id) => id],
  (items, id) => items.find((item) => item.id === id)
);

function ItemRow({ itemId }) {
  // One selector instance per mounted ItemRow β†’ each keeps its own cache.
  const selectItemById = useMemo(makeSelectItemById, []);
  const item = useSelector((state) => selectItemById(state, itemId));
  return item ? <div>{item.name}</div> : null;
}

useDispatch In-Depth

useDispatch() returns the store's dispatch function. You call it with an action β€” either a plain object or (more commonly) the result of an action creator.

graph LR A["Component"] --> B["useDispatch()"] B --> C["dispatch(action)"] C --> D["middleware"] D --> E["reducer"] E --> F["new state"]

Basic dispatch

import { useDispatch } from 'react-redux';
import { addTodo, toggleTodo } from './todoSlice';

function TodoControls() {
  const dispatch = useDispatch();
  return (
    <div>
      <button onClick={() => dispatch(addTodo('Write tests'))}>Add</button>
      <button onClick={() => dispatch(toggleTodo(1))}>Toggle #1</button>
    </div>
  );
}

Async dispatch with a thunk

To dispatch an asynchronous flow (like a fetch), you dispatch a thunk β€” a function of dispatch β€” which the thunk middleware runs. You'll formalize this in the middleware lesson; here's the shape:

// A thunk action creator: returns a function instead of a plain object.
const fetchTodos = () => async (dispatch) => {
  dispatch({ type: 'todos/fetchStart' });
  try {
    const res = await fetch('/api/todos');
    const data = await res.json();
    dispatch({ type: 'todos/fetchSuccess', payload: data });
  } catch (err) {
    dispatch({ type: 'todos/fetchFailure', payload: err.message });
  }
};

function TodoLoader() {
  const dispatch = useDispatch();
  const isLoading = useSelector((state) => state.todos.isLoading);

  return (
    <div>
      <button onClick={() => dispatch(fetchTodos())} disabled={isLoading}>
        {isLoading ? 'Loading…' : 'Load Todos'}
      </button>
    </div>
  );
}

βœ… dispatch is stable β€” no useCallback needed for identity

The dispatch reference never changes for the life of the store. You don't need to memoize a handler just to keep dispatch stable, and it's safe to list in effect dependencies:

useEffect(() => {
  dispatch(fetchTodos());
}, [dispatch]); // dispatch is stable β†’ effect runs once on mount

Use useCallback only when the handler also closes over changing values (like form text) and is passed to a memoized child.

Custom Hooks with Redux

Because useSelector and useDispatch are just hooks, you can compose them into your own. A custom hook bottles up a feature's state and actions behind one clean call β€” the single best way to keep components tidy and share Redux logic.

import { useSelector, useDispatch } from 'react-redux';
import { useCallback } from 'react';
import { login, logout } from './authSlice';

// Everything a component needs to know about auth, in one hook.
function useAuth() {
  const user = useSelector((state) => state.auth.user);
  const isLoading = useSelector((state) => state.auth.isLoading);
  const error = useSelector((state) => state.auth.error);
  const dispatch = useDispatch();

  const signIn  = useCallback((creds) => dispatch(login(creds)), [dispatch]);
  const signOut = useCallback(() => dispatch(logout()), [dispatch]);

  return { user, isLoading, error, signIn, signOut };
}

// Usage β€” the component never touches the store directly.
function LoginButton() {
  const { user, isLoading, signIn, signOut } = useAuth();
  if (user) return <button onClick={signOut}>Sign out {user.name}</button>;
  return (
    <button onClick={() => signIn({ demo: true })} disabled={isLoading}>
      {isLoading ? 'Signing in…' : 'Sign in'}
    </button>
  );
}

The same idea scales to useCart, usePosts, useNotifications β€” each pairs the relevant selectors with the actions that change them.

Practice & Quiz

πŸ‹οΈ Exercise 1: Kill the re-render bug

Goal: This component re-renders on every store update, even when neither value changed. Explain why and give two different fixes.

function ProfileHeader() {
  const { name, avatar } = useSelector((state) => ({
    name:   state.user.name,
    avatar: state.user.avatar,
  }));
  return <img src={avatar} alt={name} />;
}
πŸ’‘ Hint

The selector returns a fresh object literal each call. Compare that to how useSelector decides whether to re-render.

βœ… Solution

The object literal is a new reference every render, so the default === check is always false. Two fixes:

// Fix A β€” separate selectors, each returns a stable primitive/slice
function ProfileHeader() {
  const name   = useSelector((state) => state.user.name);
  const avatar = useSelector((state) => state.user.avatar);
  return <img src={avatar} alt={name} />;
}

// Fix B β€” keep the object, but compare it shallowly
import { shallowEqual } from 'react-redux';
function ProfileHeader() {
  const { name, avatar } = useSelector(
    (state) => ({ name: state.user.name, avatar: state.user.avatar }),
    shallowEqual
  );
  return <img src={avatar} alt={name} />;
}

πŸ‹οΈ Exercise 2: A useCart hook

Goal: Write a custom hook that exposes the cart's item count, a memoized total, and an addItem dispatcher. Assume state shape { cart: { items: [{ id, price, quantity }] } } and an action creator addItem(product).

πŸ’‘ Hint

Use createSelector for the total so it isn't recomputed on unrelated updates, useSelector for the count, and useDispatch for the action.

βœ… Solution
import { useSelector, useDispatch } from 'react-redux';
import { createSelector } from '@reduxjs/toolkit';
import { useCallback } from 'react';
import { addItem } from './cartSlice';

const selectItems = (state) => state.cart.items;
const selectTotal = createSelector([selectItems], (items) =>
  items.reduce((sum, i) => sum + i.price * i.quantity, 0)
);
const selectCount = createSelector([selectItems], (items) =>
  items.reduce((sum, i) => sum + i.quantity, 0)
);

export function useCart() {
  const count = useSelector(selectCount);
  const total = useSelector(selectTotal);
  const dispatch = useDispatch();
  const add = useCallback((product) => dispatch(addItem(product)), [dispatch]);
  return { count, total, add };
}

🎯 Quick Quiz

Question 1: By default, how does useSelector decide whether to re-render the component?

Question 2: What does createSelector give you?

Question 3: Is it safe to include dispatch in a useEffect dependency array?

Best Practices & Pitfalls

βœ… Do

  • Return primitives or stable slices from selectors whenever possible
  • Use shallowEqual when a selector must return an object of several fields
  • Memoize derived data with createSelector; use a factory for per-item selectors
  • Wrap feature logic in custom hooks (useAuth, useCart) for clean, testable components
  • Treat dispatch as stable and list it in dependency arrays as the linter suggests

❌ Don't

  • Don't return a new object/array literal from useSelector without shallowEqual or memoization
  • Don't share one createSelector instance across components that pass different arguments β€” use a factory
  • Don't do heavy computation inline in a selector on every render β€” memoize it
  • Don't reach for useStore to read state; useSelector subscribes correctly and useStore does not
  • Don't mutate the value you get back from useSelector β€” it's the live state; treat it as read-only

⚠️ useStore is a rare escape hatch

React-Redux also exposes useStore(), which returns the store itself. It does not subscribe to updates, so reading store.getState() in render won't re-render when state changes. Use it only for one-off imperative needs (like logging in an event handler); reach for useSelector for anything you render.

Summary

πŸŽ‰ Key Takeaways

  • useSelector reads a slice; useDispatch sends actions β€” together they form the read/write loop
  • Re-render is decided by === on the selector's return value β€” so prefer primitives and stable references
  • Use shallowEqual for multi-field objects and createSelector for derived data
  • Per-item memoization needs a selector factory so each component keeps its own cache
  • dispatch is stable; async flows go through thunks; feature logic belongs in custom hooks

πŸ“š Additional Resources

πŸš€ What's Next?

You've been wrapping your app in <Provider> without dwelling on how it works. The next lesson, Provider component, opens the hood: how it uses React Context, where to place it, SSR and testing setups, and the store-stability rules that keep everything humming.

🎣 Reeled it in!

You can now read, derive, and dispatch state efficiently. Fast Redux apps live and die on the equality rules you just learned.