Day 8: Selectors & Performance
What You'll Learn Today
- What selectors are and why they matter
- Simple selectors vs memoized selectors
- How
createSelectorand memoization work - Composing selectors for complex data
useSelectorand re-render behavior- Common performance pitfalls and how to avoid them
createEntityAdapterselectors- Performance debugging techniques
What Are Selectors?
Selectors are functions that extract data from the Redux store's state. They act as an abstraction layer so that components don't depend directly on the state's structure.
flowchart LR
subgraph Store["Redux Store"]
S["State"]
end
subgraph Selectors["Selectors"]
S1["selectTodos"]
S2["selectFilter"]
S3["selectFilteredTodos"]
end
subgraph Component["Component"]
C["TodoList"]
end
S --> S1
S --> S2
S1 --> S3
S2 --> S3
S3 --> C
style Store fill:#3b82f6,color:#fff
style Selectors fill:#8b5cf6,color:#fff
style Component fill:#22c55e,color:#fff
Why Use Selectors?
| Benefit | Description |
|---|---|
| Encapsulation | If the state shape changes, only the selector needs updating |
| Reusability | Multiple components share the same data extraction logic |
| Testability | Pure functions are easy to test |
| Performance | Memoization prevents unnecessary recalculations |
| Readability | Function names communicate the intent of data extraction |
Simple Selectors
The most basic selector is a function that takes state and returns a part of it.
// Selectors that extract specific parts of state
const selectTodos = (state) => state.todos.items;
const selectFilter = (state) => state.todos.filter;
const selectUserName = (state) => state.user.name;
// Using in a component
function TodoList() {
const todos = useSelector(selectTodos);
const filter = useSelector(selectFilter);
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
TypeScript version
import { useSelector } from 'react-redux';
import type { RootState } from './store';
const selectTodos = (state: RootState) => state.todos.items;
const selectFilter = (state: RootState) => state.todos.filter;
const selectUserName = (state: RootState) => state.user.name;
function TodoList() {
const todos = useSelector(selectTodos);
const filter = useSelector(selectFilter);
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
Defining Selectors in Slice Files
RTK recommends defining selectors alongside the slice they read from.
// features/todos/todosSlice.js
import { createSlice } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: {
items: [],
filter: 'all',
},
reducers: {
addTodo: (state, action) => {
state.items.push({
id: Date.now(),
text: action.payload,
completed: false,
});
},
toggleTodo: (state, action) => {
const todo = state.items.find((t) => t.id === action.payload);
if (todo) todo.completed = !todo.completed;
},
setFilter: (state, action) => {
state.filter = action.payload;
},
},
});
export const { addTodo, toggleTodo, setFilter } = todosSlice.actions;
// Selectors
export const selectTodos = (state) => state.todos.items;
export const selectFilter = (state) => state.todos.filter;
export default todosSlice.reducer;
TypeScript version
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '../../store';
interface Todo {
id: number;
text: string;
completed: boolean;
}
type FilterType = 'all' | 'active' | 'completed';
interface TodosState {
items: Todo[];
filter: FilterType;
}
const initialState: TodosState = {
items: [],
filter: 'all',
};
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: (state, action: PayloadAction<string>) => {
state.items.push({
id: Date.now(),
text: action.payload,
completed: false,
});
},
toggleTodo: (state, action: PayloadAction<number>) => {
const todo = state.items.find((t) => t.id === action.payload);
if (todo) todo.completed = !todo.completed;
},
setFilter: (state, action: PayloadAction<FilterType>) => {
state.filter = action.payload;
},
},
});
export const { addTodo, toggleTodo, setFilter } = todosSlice.actions;
export const selectTodos = (state: RootState) => state.todos.items;
export const selectFilter = (state: RootState) => state.todos.filter;
export default todosSlice.reducer;
createSelector β Memoized Selectors
createSelector from Reselect (bundled with RTK) creates memoized selectors. As long as the input selectors return the same values, the output selector returns a cached result instead of recalculating.
How Memoization Works
flowchart TB
subgraph Input["Input Selectors"]
IS1["selectTodos(state)"]
IS2["selectFilter(state)"]
end
subgraph Check["Reference Comparison"]
C{"Same as last time?"}
end
subgraph Output["Output Selector"]
OS["Compute filteredTodos"]
end
subgraph Cache["Cache"]
CR["Return cached result"]
end
IS1 --> C
IS2 --> C
C -->|"No"| OS
C -->|"Yes"| CR
style Input fill:#3b82f6,color:#fff
style Check fill:#f59e0b,color:#fff
style Output fill:#8b5cf6,color:#fff
style Cache fill:#22c55e,color:#fff
Basic Usage
import { createSelector } from '@reduxjs/toolkit';
const selectTodos = (state) => state.todos.items;
const selectFilter = (state) => state.todos.filter;
// Memoized selector
const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
// This function only runs when todos or filter changes
switch (filter) {
case 'active':
return todos.filter((t) => !t.completed);
case 'completed':
return todos.filter((t) => t.completed);
default:
return todos;
}
}
);
TypeScript version
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from './store';
const selectTodos = (state: RootState) => state.todos.items;
const selectFilter = (state: RootState) => state.todos.filter;
const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'active':
return todos.filter((t) => !t.completed);
case 'completed':
return todos.filter((t) => t.completed);
default:
return todos;
}
}
);
What Happens Without Memoization
// BAD: Returns a new array every time β re-renders every time
function TodoList() {
const activeTodos = useSelector((state) =>
state.todos.items.filter((t) => !t.completed)
);
// activeTodos is a new array reference on every call,
// so the component re-renders even if contents are the same
}
// GOOD: Use a memoized selector
function TodoList() {
const activeTodos = useSelector(selectFilteredTodos);
// Returns the same reference as long as todos/filter haven't changed
}
Composing Selectors
Memoized selectors can use other selectors as inputs, letting you build complex data step by step.
import { createSelector } from '@reduxjs/toolkit';
// Level 1: Simple selectors
const selectTodos = (state) => state.todos.items;
const selectFilter = (state) => state.todos.filter;
const selectSearchQuery = (state) => state.todos.searchQuery;
// Level 2: Apply filter
const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'active':
return todos.filter((t) => !t.completed);
case 'completed':
return todos.filter((t) => t.completed);
default:
return todos;
}
}
);
// Level 3: Apply search (uses Level 2 result)
const selectSearchedTodos = createSelector(
[selectFilteredTodos, selectSearchQuery],
(filteredTodos, query) => {
if (!query) return filteredTodos;
const lowerQuery = query.toLowerCase();
return filteredTodos.filter((t) =>
t.text.toLowerCase().includes(lowerQuery)
);
}
);
// Level 4: Statistics (combines multiple selectors)
const selectTodoStats = createSelector(
[selectTodos],
(todos) => ({
total: todos.length,
active: todos.filter((t) => !t.completed).length,
completed: todos.filter((t) => t.completed).length,
completionRate: todos.length > 0
? Math.round((todos.filter((t) => t.completed).length / todos.length) * 100)
: 0,
})
);
TypeScript version
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from './store';
const selectTodos = (state: RootState) => state.todos.items;
const selectFilter = (state: RootState) => state.todos.filter;
const selectSearchQuery = (state: RootState) => state.todos.searchQuery;
const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'active':
return todos.filter((t) => !t.completed);
case 'completed':
return todos.filter((t) => t.completed);
default:
return todos;
}
}
);
const selectSearchedTodos = createSelector(
[selectFilteredTodos, selectSearchQuery],
(filteredTodos, query) => {
if (!query) return filteredTodos;
const lowerQuery = query.toLowerCase();
return filteredTodos.filter((t) =>
t.text.toLowerCase().includes(lowerQuery)
);
}
);
interface TodoStats {
total: number;
active: number;
completed: number;
completionRate: number;
}
const selectTodoStats = createSelector(
[selectTodos],
(todos): TodoStats => ({
total: todos.length,
active: todos.filter((t) => !t.completed).length,
completed: todos.filter((t) => t.completed).length,
completionRate: todos.length > 0
? Math.round((todos.filter((t) => t.completed).length / todos.length) * 100)
: 0,
})
);
flowchart TB
subgraph L1["Level 1: Simple Selectors"]
S1["selectTodos"]
S2["selectFilter"]
S3["selectSearchQuery"]
end
subgraph L2["Level 2: Filter"]
S4["selectFilteredTodos"]
end
subgraph L3["Level 3: Search"]
S5["selectSearchedTodos"]
end
subgraph L4["Level 4: Statistics"]
S6["selectTodoStats"]
end
S1 --> S4
S2 --> S4
S4 --> S5
S3 --> S5
S1 --> S6
style L1 fill:#3b82f6,color:#fff
style L2 fill:#8b5cf6,color:#fff
style L3 fill:#22c55e,color:#fff
style L4 fill:#f59e0b,color:#fff
useSelector and Re-Renders
Reference Equality Checks
useSelector compares the selector's return value against the previous value using strict reference equality (===). The component only re-renders when the value differs.
// Reference doesn't change β no re-render
const name = useSelector((state) => state.user.name);
// Strings are compared by value, so same string means no re-render
// New object every time β re-renders every time!
const user = useSelector((state) => ({
name: state.user.name,
email: state.user.email,
}));
// {} !== {} β even with identical contents, it re-renders
Solutions
Approach 1: Split into multiple useSelector calls
function UserProfile() {
const name = useSelector((state) => state.user.name);
const email = useSelector((state) => state.user.email);
return (
<div>
<p>{name}</p>
<p>{email}</p>
</div>
);
}
Approach 2: Memoize with createSelector
const selectUserProfile = createSelector(
[(state) => state.user.name, (state) => state.user.email],
(name, email) => ({ name, email })
);
function UserProfile() {
const { name, email } = useSelector(selectUserProfile);
// Same object reference as long as name and email haven't changed
return (
<div>
<p>{name}</p>
<p>{email}</p>
</div>
);
}
Approach 3: Use shallowEqual
import { useSelector, shallowEqual } from 'react-redux';
function UserProfile() {
const { name, email } = useSelector(
(state) => ({
name: state.user.name,
email: state.user.email,
}),
shallowEqual // Use shallow comparison
);
return (
<div>
<p>{name}</p>
<p>{email}</p>
</div>
);
}
Common Performance Pitfalls
1. Creating New References in Inline Selectors
// BAD: filter() creates a new array every time
function ActiveTodos() {
const activeTodos = useSelector((state) =>
state.todos.items.filter((t) => !t.completed)
);
// Re-renders even when unrelated state changes
}
// GOOD: Use createSelector
const selectActiveTodos = createSelector(
[(state) => state.todos.items],
(todos) => todos.filter((t) => !t.completed)
);
function ActiveTodos() {
const activeTodos = useSelector(selectActiveTodos);
}
2. Creating New Arrays with map
// BAD: map always returns a new array
function TodoNames() {
const names = useSelector((state) =>
state.todos.items.map((t) => t.text)
);
}
// GOOD
const selectTodoNames = createSelector(
[(state) => state.todos.items],
(todos) => todos.map((t) => t.text)
);
3. Creating Objects Inside Selectors
// BAD: Creates a new object every time
function Dashboard() {
const stats = useSelector((state) => ({
total: state.todos.items.length,
completed: state.todos.items.filter((t) => t.completed).length,
}));
}
// GOOD: Use createSelector
const selectStats = createSelector(
[(state) => state.todos.items],
(todos) => ({
total: todos.length,
completed: todos.filter((t) => t.completed).length,
})
);
4. Calling createSelector Inside a Component
// BAD: New selector created on every render
function TodoList({ userId }) {
const todos = useSelector(
createSelector(
[(state) => state.todos.items],
(todos) => todos.filter((t) => t.userId === userId)
)
);
}
// GOOD: Memoize the selector with useMemo
function TodoList({ userId }) {
const selectUserTodos = useMemo(
() =>
createSelector(
[(state) => state.todos.items],
(todos) => todos.filter((t) => t.userId === userId)
),
[userId]
);
const todos = useSelector(selectUserTodos);
}
TypeScript version
import { useMemo } from 'react';
import { createSelector } from '@reduxjs/toolkit';
import { useSelector } from 'react-redux';
import type { RootState } from './store';
interface TodoListProps {
userId: string;
}
function TodoList({ userId }: TodoListProps) {
const selectUserTodos = useMemo(
() =>
createSelector(
[(state: RootState) => state.todos.items],
(todos) => todos.filter((t) => t.userId === userId)
),
[userId]
);
const todos = useSelector(selectUserTodos);
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
React.memo with Redux
When rendering large lists, wrapping child components in React.memo prevents unnecessary re-renders.
import { memo } from 'react';
import { useSelector } from 'react-redux';
// List item component
const TodoItem = memo(function TodoItem({ id }) {
// Use the ID to select the specific todo
const todo = useSelector((state) =>
state.todos.items.find((t) => t.id === id)
);
if (!todo) return null;
return (
<li style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</li>
);
});
// List component (only fetches the ID array)
function TodoList() {
const todoIds = useSelector((state) =>
state.todos.items.map((t) => t.id)
);
return (
<ul>
{todoIds.map((id) => (
<TodoItem key={id} id={id} />
))}
</ul>
);
}
TypeScript version
import { memo } from 'react';
import { useSelector } from 'react-redux';
import type { RootState } from './store';
interface TodoItemProps {
id: number;
}
const TodoItem = memo(function TodoItem({ id }: TodoItemProps) {
const todo = useSelector((state: RootState) =>
state.todos.items.find((t) => t.id === id)
);
if (!todo) return null;
return (
<li style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</li>
);
});
function TodoList() {
const todoIds = useSelector((state: RootState) =>
state.todos.items.map((t) => t.id)
);
return (
<ul>
{todoIds.map((id) => (
<TodoItem key={id} id={id} />
))}
</ul>
);
}
Pattern: The parent component fetches only an array of IDs, and each child component uses
useSelectorto fetch its own data. This way, when one todo changes, only thatTodoItemre-renders.
createEntityAdapter Selectors
createEntityAdapter provides CRUD operations and built-in selectors for normalized data.
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
const todosAdapter = createEntityAdapter();
const todosSlice = createSlice({
name: 'todos',
initialState: todosAdapter.getInitialState({
filter: 'all',
}),
reducers: {
addTodo: todosAdapter.addOne,
updateTodo: todosAdapter.updateOne,
removeTodo: todosAdapter.removeOne,
setAllTodos: todosAdapter.setAll,
setFilter: (state, action) => {
state.filter = action.payload;
},
},
});
// Selectors provided by the adapter
export const {
selectAll: selectAllTodos,
selectById: selectTodoById,
selectIds: selectTodoIds,
selectTotal: selectTotalTodos,
selectEntities: selectTodoEntities,
} = todosAdapter.getSelectors((state) => state.todos);
// Combine with custom selectors
const selectFilter = (state) => state.todos.filter;
export const selectFilteredTodos = createSelector(
[selectAllTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'active':
return todos.filter((t) => !t.completed);
case 'completed':
return todos.filter((t) => t.completed);
default:
return todos;
}
}
);
TypeScript version
import { createEntityAdapter, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from '../../store';
interface Todo {
id: string;
text: string;
completed: boolean;
}
type FilterType = 'all' | 'active' | 'completed';
const todosAdapter = createEntityAdapter<Todo>();
interface TodosExtraState {
filter: FilterType;
}
const todosSlice = createSlice({
name: 'todos',
initialState: todosAdapter.getInitialState<TodosExtraState>({
filter: 'all',
}),
reducers: {
addTodo: todosAdapter.addOne,
updateTodo: todosAdapter.updateOne,
removeTodo: todosAdapter.removeOne,
setAllTodos: todosAdapter.setAll,
setFilter: (state, action: PayloadAction<FilterType>) => {
state.filter = action.payload;
},
},
});
export const {
selectAll: selectAllTodos,
selectById: selectTodoById,
selectIds: selectTodoIds,
selectTotal: selectTotalTodos,
selectEntities: selectTodoEntities,
} = todosAdapter.getSelectors((state: RootState) => state.todos);
const selectFilter = (state: RootState) => state.todos.filter;
export const selectFilteredTodos = createSelector(
[selectAllTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'active':
return todos.filter((t) => !t.completed);
case 'completed':
return todos.filter((t) => t.completed);
default:
return todos;
}
}
);
Selectors Provided by Entity Adapter
| Selector | Returns | Description |
|---|---|---|
selectAll |
Entity[] |
All entities as an array |
selectById |
Entity | undefined |
A single entity by ID |
selectIds |
EntityId[] |
All IDs as an array |
selectTotal |
number |
Total number of entities |
selectEntities |
Record<EntityId, Entity> |
Normalized entity lookup object |
Example: Filtered and Sorted Product List
import { createSelector } from '@reduxjs/toolkit';
// Input selectors
const selectProducts = (state) => state.products.items;
const selectCategory = (state) => state.products.selectedCategory;
const selectSortBy = (state) => state.products.sortBy;
const selectPriceRange = (state) => state.products.priceRange;
// Step 1: Category filter
const selectCategoryFiltered = createSelector(
[selectProducts, selectCategory],
(products, category) => {
if (category === 'all') return products;
return products.filter((p) => p.category === category);
}
);
// Step 2: Price filter
const selectPriceFiltered = createSelector(
[selectCategoryFiltered, selectPriceRange],
(products, { min, max }) => {
return products.filter((p) => p.price >= min && p.price <= max);
}
);
// Step 3: Sort
const selectSortedProducts = createSelector(
[selectPriceFiltered, selectSortBy],
(products, sortBy) => {
const sorted = [...products];
switch (sortBy) {
case 'price-asc':
return sorted.sort((a, b) => a.price - b.price);
case 'price-desc':
return sorted.sort((a, b) => b.price - a.price);
case 'name':
return sorted.sort((a, b) => a.name.localeCompare(b.name));
case 'newest':
return sorted.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
default:
return sorted;
}
}
);
// Step 4: Statistics
const selectProductStats = createSelector(
[selectPriceFiltered],
(products) => ({
count: products.length,
avgPrice: products.length > 0
? Math.round(products.reduce((sum, p) => sum + p.price, 0) / products.length)
: 0,
minPrice: products.length > 0
? Math.min(...products.map((p) => p.price))
: 0,
maxPrice: products.length > 0
? Math.max(...products.map((p) => p.price))
: 0,
})
);
TypeScript version
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from './store';
interface Product {
id: string;
name: string;
price: number;
category: string;
createdAt: string;
}
type SortBy = 'price-asc' | 'price-desc' | 'name' | 'newest';
const selectProducts = (state: RootState) => state.products.items;
const selectCategory = (state: RootState) => state.products.selectedCategory;
const selectSortBy = (state: RootState) => state.products.sortBy;
const selectPriceRange = (state: RootState) => state.products.priceRange;
const selectCategoryFiltered = createSelector(
[selectProducts, selectCategory],
(products, category): Product[] => {
if (category === 'all') return products;
return products.filter((p) => p.category === category);
}
);
const selectPriceFiltered = createSelector(
[selectCategoryFiltered, selectPriceRange],
(products, { min, max }): Product[] => {
return products.filter((p) => p.price >= min && p.price <= max);
}
);
const selectSortedProducts = createSelector(
[selectPriceFiltered, selectSortBy],
(products, sortBy): Product[] => {
const sorted = [...products];
switch (sortBy) {
case 'price-asc':
return sorted.sort((a, b) => a.price - b.price);
case 'price-desc':
return sorted.sort((a, b) => b.price - a.price);
case 'name':
return sorted.sort((a, b) => a.name.localeCompare(b.name));
case 'newest':
return sorted.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
default:
return sorted;
}
}
);
interface ProductStats {
count: number;
avgPrice: number;
minPrice: number;
maxPrice: number;
}
const selectProductStats = createSelector(
[selectPriceFiltered],
(products): ProductStats => ({
count: products.length,
avgPrice: products.length > 0
? Math.round(products.reduce((sum, p) => sum + p.price, 0) / products.length)
: 0,
minPrice: products.length > 0
? Math.min(...products.map((p) => p.price))
: 0,
maxPrice: products.length > 0
? Math.max(...products.map((p) => p.price))
: 0,
})
);
Example: Dashboard with Derived Statistics
import { createSelector } from '@reduxjs/toolkit';
const selectOrders = (state) => state.orders.items;
const selectUsers = (state) => state.users.entities;
// Monthly sales
const selectMonthlySales = createSelector(
[selectOrders],
(orders) => {
const monthly = {};
orders.forEach((order) => {
const month = order.date.slice(0, 7); // "2025-01"
monthly[month] = (monthly[month] || 0) + order.total;
});
return Object.entries(monthly)
.map(([month, total]) => ({ month, total }))
.sort((a, b) => a.month.localeCompare(b.month));
}
);
// Top customers
const selectTopCustomers = createSelector(
[selectOrders, selectUsers],
(orders, users) => {
const spending = {};
orders.forEach((order) => {
spending[order.userId] = (spending[order.userId] || 0) + order.total;
});
return Object.entries(spending)
.map(([userId, total]) => ({
user: users[userId],
totalSpent: total,
}))
.sort((a, b) => b.totalSpent - a.totalSpent)
.slice(0, 10);
}
);
// Full dashboard summary
const selectDashboardSummary = createSelector(
[selectOrders, selectMonthlySales, selectTopCustomers],
(orders, monthlySales, topCustomers) => ({
totalRevenue: orders.reduce((sum, o) => sum + o.total, 0),
orderCount: orders.length,
averageOrderValue: orders.length > 0
? Math.round(orders.reduce((sum, o) => sum + o.total, 0) / orders.length)
: 0,
monthlySales,
topCustomers,
})
);
Performance Debugging
Redux DevTools
Use the "Diff" tab in Redux DevTools to see which parts of state each action changes. Check for unnecessary state updates.
React DevTools Profiler
- Open the Profiler tab in React DevTools
- Enable Settings > Highlight updates when components render
- Perform actions and observe which components re-render
Checking Selector Recomputations
// Development only: track how often a selector recomputes
const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
console.log('selectFilteredTodos recomputed!');
switch (filter) {
case 'active':
return todos.filter((t) => !t.completed);
case 'completed':
return todos.filter((t) => t.completed);
default:
return todos;
}
}
);
// Reselect debugging API
console.log(selectFilteredTodos.recomputations()); // Number of recomputations
selectFilteredTodos.resetRecomputations(); // Reset the counter
why-did-you-render Library
// setupTests.js
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
Best Practices
| Practice | Description |
|---|---|
| Define selectors in slice files | Keep selectors close to the state shape they read |
Use createSelector for derived data |
Memoize any array or object creation |
| Inline selectors only for simple values | state.user.name is fine inline |
| ID list + individual fetch pattern | For large lists, parent gets IDs, children get data |
useMemo for parameterized selectors |
Wrap selectors that depend on props in useMemo |
shallowEqual as a last resort |
Try createSelector first |
| Measure recomputations | Use recomputations() to check selector efficiency |
| Normalize state | Use createEntityAdapter to avoid duplication |
Summary
Today we covered selectors and performance optimization in Redux.
| Concept | Description |
|---|---|
| Selector | A function that extracts data from state |
| Simple selector | state => state.slice.field format |
| createSelector | Memoizes based on input selector results |
| Memoization | Returns cached result when inputs haven't changed |
| Selector composition | Combine selectors to build complex data step by step |
| useSelector | Uses strict equality (===) to decide re-renders |
| shallowEqual | Shallow object comparison for re-render optimization |
| createEntityAdapter | Provides CRUD operations and selectors for normalized data |
Key takeaways:
- Selectors serve as an abstraction layer over your state shape
- Memoize operations that create new references (
filter(),map(), object literals) withcreateSelector - Never return new objects or arrays from inline selectors passed to
useSelector - For large lists, use the "ID list + individual fetch" pattern
- Measure before optimizing β use profiling tools to find real bottlenecks
Exercises
Exercise 1: Creating Memoized Selectors
Create selectors using createSelector that:
- Filter only active users from
state.users.items - Sort users alphabetically by name
- Return a stats object with total user count and active user count
Exercise 2: Performance Fix
The following component has performance issues. Identify the problems and fix them:
function ProductList() {
const products = useSelector((state) =>
state.products.items
.filter((p) => p.inStock)
.map((p) => ({
...p,
discountedPrice: p.price * 0.9,
}))
.sort((a, b) => a.name.localeCompare(b.name))
);
return (
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name}: ${p.discountedPrice}
</li>
))}
</ul>
);
}
Exercise 3: Entity Adapter Selectors
Build a blog post management system using createEntityAdapter:
- Posts have a category and a published status
- Create a selector that returns posts by category
- Create a selector that returns the count of published posts
Exercise 4: Parameterized Selectors
Create a selector that takes a user ID and returns that user's orders. Use useMemo to properly handle the selector's dependency on component props.