π£ 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
useSelectordecides whether to re-render, and change that with an equality function - Build memoized selectors with
createSelectorand parameterized/factory selectors for per-item data - Dispatch synchronous and async (thunk) actions with
useDispatch - Know why
dispatchis stable and how that affectsuseEffect/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.
π£ 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.
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.
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
shallowEqualwhen 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
dispatchas stable and list it in dependency arrays as the linter suggests
β Don't
- Don't return a new object/array literal from
useSelectorwithoutshallowEqualor memoization - Don't share one
createSelectorinstance 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
useStoreto read state;useSelectorsubscribes correctly anduseStoredoes 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
useSelectorreads a slice;useDispatchsends 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
shallowEqualfor multi-field objects andcreateSelectorfor derived data - Per-item memoization needs a selector factory so each component keeps its own cache
dispatchis stable; async flows go through thunks; feature logic belongs in custom hooks
π Additional Resources
- React-Redux β Hooks API reference
- React-Redux β useSelector in detail
- React-Redux β useDispatch recipes
- Redux β Deriving Data with Selectors (Reselect)
π 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.