Reselect in Redux
Reselect is a library for creating memoized selectors in Redux. It helps optimize performance by caching selector results and only recomputing when the input selectors return new values.
Table of Contents
What is Reselect?
- Reselect is a library that provides a way to create memoized selectors.
- It caches the result of a selector and only recomputes when the input selectors return new values.
- Helps prevent unnecessary re-computations and improves performance.
- Built on top of the concept of “derived data” in Redux.
Installation:
npm install reselect
How does Reselect Work?
- Input Selectors: Define selectors that extract the data you need.
- Result Function: Define a function that computes the derived data from the input selectors.
- Memoization: Reselect caches the result and only recomputes when input selectors change.
- Performance: Avoids expensive computations on every state change.
Signature:
import { createSelector } from 'reselect';
const selector = createSelector(
[inputSelector1, inputSelector2, ...],
(result1, result2, ...) => computedResult
);
Usage Examples
Basic memoized selector:
import { createSelector } from 'reselect';
// Input selectors
const selectTodos = (state) => state.todos;
const selectFilter = (state) => state.filter;
// Memoized selector
const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'completed':
return todos.filter(todo => todo.completed);
case 'active':
return todos.filter(todo => !todo.completed);
default:
return todos;
}
}
);
Composed selectors:
import { createSelector } from 'reselect';
// Base selectors
const selectTodos = (state) => state.todos;
const selectFilter = (state) => state.filter;
// Memoized filtered todos
const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'completed':
return todos.filter(todo => todo.completed);
case 'active':
return todos.filter(todo => !todo.completed);
default:
return todos;
}
}
);
// Memoized stats (uses another memoized selector)
const selectTodoStats = createSelector(
[selectFilteredTodos],
(filteredTodos) => {
const total = filteredTodos.length;
const completed = filteredTodos.filter(todo => todo.completed).length;
const active = total - completed;
return {
total,
completed,
active,
completionRate: total > 0 ? (completed / total) * 100 : 0
};
}
);
Using in components:
import { useSelector } from 'react-redux';
import { selectFilteredTodos, selectTodoStats } from './selectors';
const TodoList = () => {
const filteredTodos = useSelector(selectFilteredTodos);
const stats = useSelector(selectTodoStats);
return (
<div>
<p>Total: {stats.total}</p>
<p>Completed: {stats.completed}</p>
<p>Active: {stats.active}</p>
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
};
Selector with parameters:
import { createSelector } from 'reselect';
const selectTodos = (state) => state.todos;
// Factory function for parameterized selector
const makeSelectTodoById = () => createSelector(
[selectTodos, (state, todoId) => todoId],
(todos, todoId) => todos.find(todo => todo.id === todoId)
);
// Usage
const selectTodoById = makeSelectTodoById();
const todo = useSelector(state => selectTodoById(state, 123));
Memoization Benefits
Performance improvement:
- Avoids expensive computations on every render.
- Only recomputes when dependencies actually change.
- Reduces unnecessary re-renders in React components.
Example without reselect:
// This runs on every state change
const selectExpensiveComputation = (state) => {
console.log('Computing expensive result...');
return state.todos
.filter(todo => !todo.completed)
.map(todo => ({ ...todo, priority: calculatePriority(todo) }))
.sort((a, b) => b.priority - a.priority);
};
Example with reselect:
// This only runs when todos change
const selectExpensiveComputation = createSelector(
[selectTodos],
(todos) => {
console.log('Computing expensive result...');
return todos
.filter(todo => !todo.completed)
.map(todo => ({ ...todo, priority: calculatePriority(todo) }))
.sort((a, b) => b.priority - a.priority);
}
);
Best Practices
- Use input selectors: Extract the data you need with simple selectors first.
- Keep result functions pure: Don’t have side effects in your selector functions.
- Compose selectors: Build complex selectors from simpler ones.
- Use factory functions: For selectors that need parameters.
- Test selectors: Ensure they work correctly with different state shapes.
Good example:
// Composable and memoized
const selectTodos = (state) => state.todos;
const selectFilter = (state) => state.filter;
const selectFilteredTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'completed':
return todos.filter(todo => todo.completed);
case 'active':
return todos.filter(todo => !todo.completed);
default:
return todos;
}
}
);
const selectTodoCount = createSelector(
[selectFilteredTodos],
(filteredTodos) => filteredTodos.length
);
Bad example:
// Don't access state directly in createSelector
const selectFilteredTodos = createSelector(
[(state) => state.todos, (state) => state.filter], //
(todos, filter) => {
// Complex logic mixed with data access
return todos.filter(todo => {
if (filter === 'completed') return todo.completed;
if (filter === 'active') return !todo.completed;
return true;
});
}
);
Common Interview Questions
Q: What is the difference between a regular selector and a reselect selector?
- Regular selectors run on every call, while reselect selectors are memoized and only recompute when dependencies change.
Q: When should you use reselect?
- Use reselect for expensive computations or when you want to prevent unnecessary re-renders.
Q: How does reselect determine when to recompute?
- Reselect uses shallow equality checks on the results of input selectors to determine if recomputation is needed.
Q: Can you use reselect with parameters?
- Yes, you can create factory functions that return parameterized selectors.
Q: What are the performance benefits of reselect?
- Avoids expensive computations, reduces unnecessary re-renders, and improves overall application performance.
Summary
- Reselect provides memoized selectors for Redux applications.
- It caches results and only recomputes when dependencies change.
- Use input selectors to extract data and result functions to compute derived data.
- Helps improve performance by avoiding expensive computations.
- Follow best practices for composable and maintainable selectors.
Interview angle
- “What does reselect do?” - memoizes derived state so an expensive computation or a newly-allocated object is not recreated on every store change. Without it, a selector returning
items.filter(...)returns a new array reference each call and re-renders every consumer. - “What was the cache-size-1 problem?” - the classic
createSelectormemoized only the last call, so two components using the same selector with different arguments thrashed it. Reselect 5 changed the default to a WeakMap-based memoizer, which handles that case; older advice tells you to build a selector factory per component instead. - “Do you need it for a plain field?” - no.
useSelector(s => s.user.name)returns a primitive compared by===; memoizing it adds cost for nothing. Reselect earns its place when the selector computes or allocates. - “Where should selectors live?” - next to the slice that owns the state, exported by name. Components importing a named selector rather than reaching into the state shape is what makes the shape refactorable.