π Selectors and Reselect
A component shouldn't know that the cart total lives at state.cart.items or how to add it up. Selectors are the functions that answer "what does the UI need?" in one place β and reselect's createSelector makes sure expensive answers are computed once and reused until the underlying data actually changes.
Week 6 · Day 5 (Friday: Advanced Redux Patterns) · Lecture 2
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain what a selector is and why it belongs between components and the store
- Write simple, computed, and parameterized selectors
- Describe how
createSelectormemoizes results using an inputβoutput cache - Compose selectors so complex derived data is built from small, testable pieces
- Build parameterized memoized selectors correctly with the factory pattern
- Avoid the classic pitfall of returning a brand-new array/object on every call
Estimated Time: 70 minutes
Project: Build memoized selectors for a filtered, sorted product list and verify they cache.
In This Lesson
What Is a Selector?
A selector is just a function that takes the Redux state and returns some slice of it β or something computed from it. state => state.user is a selector. So is a function that filters todos, sums a cart, or joins a post to its author. The point is that all knowledge of your state's shape lives in these functions, not scattered across your components.
π The Search Engine Analogy
Think of your store as a vast library and selectors as saved searches over it. A component doesn't wander the stacks; it runs a named query β "give me the visible todos," "give me the cart total" β and gets back exactly what it needs. And like a good search engine, reselect caches results: ask the same question against unchanged data and it returns the previous answer instantly instead of recomputing it.
This lesson picks up right where normalization left off. That flat byId store is fast to update but needs reassembly to render β selectors are how you reassemble it, and memoization is how you keep that reassembly cheap.
Why Selectors Matter
Without selectors: logic leaks into components
// β The component knows the state shape AND owns the logic
function TodoList() {
const todos = useSelector(state => state.todos.items);
const filter = useSelector(state => state.todos.filter);
// Filtering logic buried in the component β and copied into every
// other component that also needs "visible" todos.
const visibleTodos = todos.filter(todo => {
if (filter === 'completed') return todo.completed;
if (filter === 'active') return !todo.completed;
return true;
});
// Expensive stats recomputed on EVERY render, even unrelated ones.
const stats = {
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length
};
return <TodoView todos={visibleTodos} stats={stats} />;
}
With selectors: logic centralized and cached
import { createSelector } from 'reselect';
// (Redux Toolkit re-exports createSelector, so you can also import it
// from '@reduxjs/toolkit' β same function.)
// Input selectors: cheap, pull raw state.
const selectTodos = state => state.todos.items;
const selectFilter = state => state.todos.filter;
// Memoized: only re-runs when todos OR filter change.
export const selectVisibleTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
if (filter === 'completed') return todos.filter(t => t.completed);
if (filter === 'active') return todos.filter(t => !t.completed);
return todos;
}
);
export const selectTodoStats = createSelector(
[selectTodos],
(todos) => ({
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length,
percentComplete: todos.length
? Math.round((todos.filter(t => t.completed).length / todos.length) * 100)
: 0
})
);
// β
The component becomes a thin, declarative view
function TodoList() {
const visibleTodos = useSelector(selectVisibleTodos);
const stats = useSelector(selectTodoStats);
return <TodoView todos={visibleTodos} stats={stats} />;
}
β Four wins at once
The state shape is hidden (refactor the store without touching components), the logic lives in one testable place, the results are memoized, and any component can reuse the exact same query. Selectors are the seam between "how data is stored" and "how data is shown."
Basic Selector Patterns
1. Simple selectors
The smallest selectors just reach into state. Keep these trivial β they're the inputs your memoized selectors depend on.
const selectUser = state => state.user;
const selectUserProfile = state => state.user.profile;
// Provide a fallback for optional slices:
const selectNotifications = state => state.notifications ?? [];
const selectTheme = state => state.ui.theme ?? 'light';
2. Computed selectors
These derive a value from state. If the computation is non-trivial, memoize it (next section) so it doesn't run on every render.
// Sum a cart. Cheap, but a great memoization candidate.
const selectCartTotal = state =>
state.cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
// Filter and sort return NEW arrays β always memoize these
// so referential equality holds when inputs are unchanged.
const selectPublishedPosts = state =>
state.posts.filter(post => post.published);
3. Parameterized selectors
Sometimes a selector needs an argument β an id, a category. The simplest form takes extra args after state:
// Extra arguments come after state.
const selectPostsByUser = (state, userId) =>
state.posts.filter(post => post.authorId === userId);
// Multiple parameters work the same way.
const selectFilteredTodos = (state, filter, searchTerm) => {
let result = state.todos;
if (filter !== 'all') {
result = result.filter(t => filter === 'completed' ? t.completed : !t.completed);
}
if (searchTerm) {
const q = searchTerm.toLowerCase();
result = result.filter(t => t.text.toLowerCase().includes(q));
}
return result;
};
β οΈ Plain parameterized selectors don't memoize well
A hand-written (state, id) => selector recomputes every call. To memoize a parameterized selector you need the factory pattern β covered below. First, let's see exactly how memoization works.
How Reselect Memoizes
createSelector takes one or more input selectors and a result function. On each call it runs the inputs, then compares each result to the previous call using === (reference equality). If every input is unchanged, it skips the result function entirely and returns the cached output. Only when an input differs does it recompute β and cache the new answer.
import { createSelector } from 'reselect';
const selectTodos = state => state.todos;
const selectFilter = state => state.filter;
const selectVisibleTodos = createSelector(
[selectTodos, selectFilter], // input selectors
(todos, filter) => { // result function
console.log('Computing visible todosβ¦'); // logs ONLY on recompute
if (filter === 'completed') return todos.filter(t => t.completed);
if (filter === 'active') return todos.filter(t => !t.completed);
return todos;
}
);
A simplified mental model of the internals
function createSelector(inputSelectors, resultFunc) {
let lastInputs = null;
let lastResult = null;
return (state, ...args) => {
const inputs = inputSelectors.map(fn => fn(state, ...args));
const unchanged = lastInputs &&
inputs.every((val, i) => val === lastInputs[i]); // === reference check
if (unchanged) return lastResult; // β cache hit
lastResult = resultFunc(...inputs); // β recompute
lastInputs = inputs;
return lastResult;
};
}
Memoization in action
const state1 = {
todos: [{ id: 1, text: 'Learn Redux', completed: true },
{ id: 2, text: 'Learn Reselect', completed: false }],
filter: 'all'
};
selectVisibleTodos(state1); // logs "Computing visible todosβ¦" (miss)
selectVisibleTodos(state1); // (no log β cache hit, same references)
const state2 = { ...state1, filter: 'completed' };
selectVisibleTodos(state2); // logs "Computing visible todosβ¦" (filter changed)
const state3 = { ...state2, somethingElse: 42 };
selectVisibleTodos(state3); // (no log β todos & filter are unchanged)
π‘ Why reference equality demands immutable updates
Memoization compares with ===, so it only works if your reducers create new references when data changes and keep the same reference when it doesn't. Redux Toolkit's Immer-powered reducers do exactly this β which is why RTK and reselect are such a natural pair.
Composing Selectors
Because a memoized selector is itself just a function of state, you can feed it into another createSelector as an input. This lets you build complex derived data from small, individually-cached, individually-testable pieces.
import { createSelector } from 'reselect';
const selectUsers = state => state.users.entities;
const selectPosts = state => state.posts;
const selectComments = state => state.comments;
// Layer 1: attach each post's author.
const selectPostsWithAuthors = createSelector(
[selectPosts, selectUsers],
(posts, usersById) =>
posts.map(post => ({ ...post, author: usersById[post.authorId] }))
);
// Layer 2: build on layer 1 to attach comments.
const selectPostsWithDetails = createSelector(
[selectPostsWithAuthors, selectComments],
(posts, comments) =>
posts.map(post => {
const postComments = comments.filter(c => c.postId === post.id);
return { ...post, comments: postComments, commentCount: postComments.length };
})
);
Each layer only recomputes when its inputs change. If a comment is added, layer 2 recomputes but layer 1's author-joining is served straight from cache. That's the compounding payoff of composition.
Structured selectors
When a component needs several values at once, createStructuredSelector bundles selectors into one object-returning selector:
import { createSelector, createStructuredSelector } from 'reselect';
const selectDashboard = createStructuredSelector({
user: state => state.user,
loading: state => state.ui.loading,
cartItemCount: createSelector(
state => state.cart.items,
items => items.reduce((n, i) => n + i.quantity, 0)
)
});
function Dashboard() {
const { user, loading, cartItemCount } = useSelector(selectDashboard);
if (loading) return <Spinner />;
return <Header user={user} badge={cartItemCount} />;
}
Parameterized Selectors Done Right
A single memoized selector has a cache of size one. If two components call selectPostById(state, 1) and selectPostById(state, 2) in alternation, they'd fight over that one cache slot and each recompute constantly. The factory pattern solves this: create a fresh selector instance per component.
import { createSelector } from 'reselect';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
// A factory: each call returns a NEW selector with its own cache.
const makeSelectPostById = () =>
createSelector(
[
state => state.posts.entities,
(state, postId) => postId // the parameter becomes an input
],
(postsById, postId) => postsById[postId]
);
function PostCard({ postId }) {
// useMemo gives THIS component its own selector instance for its lifetime.
const selectPostById = useMemo(makeSelectPostById, []);
const post = useSelector(state => selectPostById(state, postId));
return <article>{post?.title}</article>;
}
π‘ Modern reselect: createSelector with a bigger cache
Reselect v5 lets you configure a larger cache so one shared selector can memoize several arguments at once, softening the need for factories:
import { createSelector, lruMemoize } from 'reselect';
const selectPostById = createSelector(
[state => state.posts.entities, (state, id) => id],
(postsById, id) => postsById[id],
{ memoize: lruMemoize, memoizeOptions: { maxSize: 10 } } // remember 10 ids
);
π‘ Rule of thumb: One selector instance = one cache slot. If the same selector is called with different arguments from different components, give each its own instance (factory) or widen the cache with maxSize.
Testing Selectors
Selectors are pure functions of state, which makes them delightful to test β no store, no mounting, no mocking. Feed in a plain state object and assert on the output. You can even assert that memoization holds by checking reference equality.
import { selectVisibleTodos, selectTodoStats } from './todoSelectors';
describe('todo selectors', () => {
const state = {
todos: {
items: [
{ id: 1, text: 'Learn Redux', completed: true },
{ id: 2, text: 'Learn Reselect', completed: false },
{ id: 3, text: 'Build App', completed: false }
],
filter: 'all'
}
};
test('returns all todos when filter is "all"', () => {
expect(selectVisibleTodos(state)).toHaveLength(3);
});
test('filters to completed todos', () => {
const s = { todos: { ...state.todos, filter: 'completed' } };
expect(selectVisibleTodos(s)).toHaveLength(1);
});
test('computes correct stats', () => {
expect(selectTodoStats(state)).toEqual({
total: 3, completed: 1, active: 2, percentComplete: 33
});
});
test('memoizes: same state returns the same reference', () => {
const a = selectVisibleTodos(state);
const b = selectVisibleTodos(state);
expect(a).toBe(b); // cache hit β identical reference
});
});
You can also prove the result function ran only when it should have, using a spy:
test('expensive work runs once for unchanged input', () => {
const compute = jest.fn(items => items.reduce((n, i) => n + i.value, 0));
const selectSum = createSelector(state => state.items, compute);
const state = { items: [{ value: 1 }, { value: 2 }] };
selectSum(state);
selectSum(state);
expect(compute).toHaveBeenCalledTimes(1); // memoized
selectSum({ items: [{ value: 3 }] });
expect(compute).toHaveBeenCalledTimes(2); // recomputed on change
});
Practice & Quiz
ποΈ Exercise 1: A memoized total
Goal: Convert this recompute-every-time selector into a memoized one with createSelector.
// Before β runs on every render:
const selectCartTotal = state =>
state.cart.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
// TODO: rewrite with createSelector so it only recomputes
// when state.cart.items changes.
π‘ Hint
Use one input selector, state => state.cart.items, and put the reduce in the result function.
β Solution
import { createSelector } from 'reselect';
const selectCartItems = state => state.cart.items;
const selectCartTotal = createSelector(
[selectCartItems],
(items) => items.reduce((sum, i) => sum + i.price * i.quantity, 0)
);
ποΈ Exercise 2: Filtered + sorted products
Goal: Compose two memoized selectors β one that filters products by an in-stock flag, and one that sorts the filtered result by price ascending.
π‘ Hint
Make selectInStock depend on the products input. Make selectSortedByPrice take selectInStock as its input, then copy the array before .sort() (sort mutates!).
β Solution
import { createSelector } from 'reselect';
const selectProducts = state => state.products.items;
const selectInStock = createSelector(
[selectProducts],
(products) => products.filter(p => p.stock > 0)
);
const selectSortedByPrice = createSelector(
[selectInStock],
(products) => [...products].sort((a, b) => a.price - b.price)
);
π― Quick Quiz
Question 1: When does a createSelector re-run its result function?
Question 2: A single createSelector instance caches how many results by default?
Question 3: Why must a filtering selector be memoized rather than written inline in the component?
Best Practices & Pitfalls
β Do
- Use
createSelectorfor any derived value that filters, maps, sorts, or joins - Keep input selectors trivial β they run on every call
- Compose small selectors into bigger ones for reuse and layered caching
- Use the factory pattern (or
maxSize) for parameterized memoized selectors - Test selectors directly with plain state objects β and assert memoization
- Co-locate selectors with their slice and export them for the whole app
β Don't
- Create new objects/arrays inside an input selector β it defeats memoization
- Call
createSelectorinside a component body (a new instance every render = no cache) - Over-memoize a trivial
state => state.xβ the overhead isn't worth it - Return the whole state and slice it in the component
- Ignore reselect's dev warning about a selector recomputing too often
β οΈ The #1 mistake: a new reference in an input selector
// β This input returns a brand-new array every call β memoization never hits
const selectIds = createSelector(
[state => state.users.map(u => u.id)], // new array each time!
ids => ids
);
// β
Depend on the stable source; do the mapping in the result function
const selectIds = createSelector(
[state => state.users],
users => users.map(u => u.id)
);
Summary
π Key Takeaways
- A selector encapsulates how to read and derive data, hiding state shape from components
createSelectormemoizes via an inputβoutput cache, comparing inputs with===- Memoization relies on immutable updates β which is exactly what RTK gives you
- Compose small selectors so complex derived data is built from cached pieces
- Parameterized memoized selectors need a factory or a widened
maxSize - The classic bug is returning a new reference from an input selector β never do that
π Additional Resources
- Redux β Deriving Data with Selectors
- Reselect β official documentation
- Redux Toolkit β createSelector
- React-Redux β useSelector & performance
π What's Next?
With a normalized store and memoized selectors, your data layer is clean and fast. Now you need to see it working. The next lesson, Redux DevTools, sets up the extension that lets you inspect every action, diff state changes, and time-travel through your app's history.
π Selectively brilliant!
You can now derive any shape your UI needs without recomputing it a thousand times a second.