Understanding Selectors
Selectors are functions that extract and/or compute derived data from the Redux store. They help encapsulate the knowledge about the state structure and provide a consistent way to access state.
🔍 The Search Engine Analogy
Think of selectors like search engines for your Redux store:
- State: The entire internet of data
- Selectors: Search queries that find specific information
- Memoization: Cached search results for faster access
- Derived Data: Search results with relevance ranking
- Reselect: Advanced search engine with smart caching
Just as search engines efficiently find and cache results, selectors efficiently extract and cache computed values from your state.
Why Use Selectors?
graph TD
A[Without Selectors] --> B[Direct State Access]
A --> C[Scattered Logic]
A --> D[Repeated Computations]
A --> E[Tight Coupling]
F[With Selectors] --> G[Encapsulated Access]
F --> H[Centralized Logic]
F --> I[Memoized Results]
F --> J[Loose Coupling]
style A fill:#f96
style F fill:#9f9
Problems Without Selectors
// Without selectors - problems
const TodoList = () => {
const todos = useSelector(state => state.todos.items);
const filter = useSelector(state => state.todos.filter);
// Logic scattered in components
const visibleTodos = todos.filter(todo => {
switch (filter) {
case 'SHOW_COMPLETED':
return todo.completed;
case 'SHOW_ACTIVE':
return !todo.completed;
default:
return true;
}
});
// Expensive computation repeated on every render
const stats = {
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length,
percentComplete: Math.round(
(todos.filter(t => t.completed).length / todos.length) * 100
)
};
return (
<div>
<TodoStats stats={stats} />
<TodoItems todos={visibleTodos} />
</div>
);
};
// Problems:
// 1. State structure exposed to components
// 2. Logic duplicated across components
// 3. Computations run on every render
// 4. Hard to change state structure
Benefits With Selectors
// With selectors - benefits
import { createSelector } from 'reselect';
// Basic selectors
const selectTodos = state => state.todos.items;
const selectFilter = state => state.todos.filter;
// Memoized selector
const selectVisibleTodos = createSelector(
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
default:
return todos;
}
}
);
// Memoized computed values
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
})
);
// Clean component
const TodoList = () => {
const visibleTodos = useSelector(selectVisibleTodos);
const stats = useSelector(selectTodoStats);
return (
<div>
<TodoStats stats={stats} />
<TodoItems todos={visibleTodos} />
</div>
);
};
// Benefits:
// 1. State structure hidden from components
// 2. Logic centralized in selectors
// 3. Computations memoized and cached
// 4. Easy to refactor state structure
Basic Selector Patterns
1. Simple Selectors
// Direct state access
const selectUser = state => state.user;
const selectPosts = state => state.posts;
const selectComments = state => state.comments;
// Nested state access
const selectUserProfile = state => state.user.profile;
const selectUserSettings = state => state.user.settings;
// With default values
const selectNotifications = state => state.notifications || [];
const selectTheme = state => state.ui.theme || 'light';
// With type checking
const selectSafeUser = state => {
if (!state.user || typeof state.user !== 'object') {
return null;
}
return state.user;
};
2. Computed Selectors
// Without memoization (recalculates every time)
const selectTotalPrice = state => {
return state.cart.items.reduce(
(total, item) => total + item.price * item.quantity,
0
);
};
// With optional filtering
const selectPublishedPosts = state => {
return state.posts.filter(post => post.published);
};
// With sorting
const selectSortedTodos = state => {
return [...state.todos].sort((a, b) =>
a.priority - b.priority
);
};
// Combining multiple state slices
const selectUserWithPosts = state => {
return {
...state.user,
posts: state.posts.filter(post => post.authorId === state.user.id)
};
};
3. Parameterized Selectors
// Selector factory pattern
const makeSelectItemById = () => {
return (state, id) => {
return state.items.find(item => item.id === id);
};
};
// Usage in component
const ItemDetail = ({ itemId }) => {
const selectItemById = useMemo(makeSelectItemById, []);
const item = useSelector(state => selectItemById(state, itemId));
return <div>{item.name}</div>;
};
// Direct parameterized selector
const selectPostsByUser = (state, userId) => {
return state.posts.filter(post => post.authorId === userId);
};
// Multiple parameters
const selectFilteredTodos = (state, filter, searchTerm) => {
let filtered = state.todos;
if (filter !== 'all') {
filtered = filtered.filter(todo =>
filter === 'completed' ? todo.completed : !todo.completed
);
}
if (searchTerm) {
filtered = filtered.filter(todo =>
todo.text.toLowerCase().includes(searchTerm.toLowerCase())
);
}
return filtered;
};
Understanding Reselect
graph TD
A[Component Renders] --> B{Input Selectors Changed?}
B -->|No| C[Return Cached Result]
B -->|Yes| D[Recompute Result]
D --> E[Cache New Result]
E --> F[Return New Result]
C --> G[Component Gets Data]
F --> G
style B fill:#f96
style C fill:#9f9
style D fill:#9cf
How Reselect Works
// Reselect's createSelector
import { createSelector } from 'reselect';
// Basic usage
const selectTodos = state => state.todos;
const selectFilter = state => state.filter;
const selectVisibleTodos = createSelector(
[selectTodos, selectFilter], // Input selectors
(todos, filter) => { // Result function
// This only runs if todos or filter changed
console.log('Computing visible todos...');
switch (filter) {
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed);
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed);
default:
return todos;
}
}
);
// How it works internally (simplified)
function createSelector(inputSelectors, resultFunc) {
let lastInputs = [];
let lastResult = null;
return (state) => {
// Get current inputs
const currentInputs = inputSelectors.map(selector => selector(state));
// Check if inputs changed
const inputsChanged = !currentInputs.every((input, index) =>
input === lastInputs[index]
);
if (inputsChanged) {
// Recompute result
lastResult = resultFunc(...currentInputs);
lastInputs = currentInputs;
}
// Return cached or new result
return lastResult;
};
}
Memoization in Action
// Example showing memoization benefits
const state1 = {
todos: [
{ id: 1, text: 'Learn Redux', completed: true },
{ id: 2, text: 'Learn Reselect', completed: false }
],
filter: 'SHOW_ALL'
};
// First call - computes result
const result1 = selectVisibleTodos(state1);
console.log('First result:', result1);
// Logs: "Computing visible todos..."
// Same state - returns cached result
const result2 = selectVisibleTodos(state1);
console.log('Second result:', result2);
// No computation log - returned cached result
// Changed filter - recomputes
const state2 = { ...state1, filter: 'SHOW_COMPLETED' };
const result3 = selectVisibleTodos(state2);
console.log('Third result:', result3);
// Logs: "Computing visible todos..."
// Unrelated state change - returns cached result
const state3 = { ...state2, unrelated: 'data' };
const result4 = selectVisibleTodos(state3);
console.log('Fourth result:', result4);
// No computation log - inputs didn't change
Advanced Reselect Patterns
1. Composing Selectors
// Base selectors
const selectUsers = state => state.users;
const selectPosts = state => state.posts;
const selectComments = state => state.comments;
// Composed selectors
const selectPostsWithAuthors = createSelector(
[selectPosts, selectUsers],
(posts, users) => {
return posts.map(post => ({
...post,
author: users[post.authorId]
}));
}
);
const selectPostsWithDetails = createSelector(
[selectPostsWithAuthors, selectComments],
(postsWithAuthors, comments) => {
return postsWithAuthors.map(post => ({
...post,
comments: comments.filter(comment => comment.postId === post.id),
commentCount: comments.filter(comment => comment.postId === post.id).length
}));
}
);
// Multi-level composition
const selectFeedData = createSelector(
[selectPostsWithDetails, selectUser],
(posts, currentUser) => {
return {
posts: posts.filter(post =>
post.author.following.includes(currentUser.id) ||
post.author.id === currentUser.id
),
currentUser
};
}
);
2. Parameterized Selectors with Reselect
// Factory pattern for parameterized selectors
const makeSelectTodoById = () => {
return createSelector(
[
state => state.todos,
(state, id) => id
],
(todos, id) => todos.find(todo => todo.id === id)
);
};
// Component usage
const TodoDetail = ({ todoId }) => {
// Create selector instance once
const selectTodoById = useMemo(makeSelectTodoById, []);
// Use selector with parameters
const todo = useSelector(state => selectTodoById(state, todoId));
return <div>{todo?.text}</div>;
};
// Alternative: Props-based selector
const makeSelectPostsByAuthor = () => {
return createSelector(
[
state => state.posts,
(state, props) => props.authorId
],
(posts, authorId) => posts.filter(post => post.authorId === authorId)
);
};
// Connected component usage
const mapStateToProps = () => {
const selectPostsByAuthor = makeSelectPostsByAuthor();
return (state, props) => ({
posts: selectPostsByAuthor(state, props)
});
};
3. Structured Selectors
import { createSelector, createStructuredSelector } from 'reselect';
// Individual selectors
const selectUser = state => state.user;
const selectIsLoading = state => state.ui.loading;
const selectError = state => state.ui.error;
const selectTotalItems = createSelector(
state => state.cart.items,
items => items.reduce((sum, item) => sum + item.quantity, 0)
);
// Structured selector - combines multiple selectors
const selectDashboardData = createStructuredSelector({
user: selectUser,
loading: selectIsLoading,
error: selectError,
cartItemCount: selectTotalItems,
recentPosts: createSelector(
state => state.posts,
posts => posts.slice(0, 5)
)
});
// Usage in component
const Dashboard = () => {
const {
user,
loading,
error,
cartItemCount,
recentPosts
} = useSelector(selectDashboardData);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
return (
<div>
<UserProfile user={user} />
<CartBadge count={cartItemCount} />
<RecentPosts posts={recentPosts} />
</div>
);
};
4. Custom Memoization
import { createSelectorCreator, defaultMemoize } from 'reselect';
import { isEqual } from 'lodash';
// Custom equality check for deep comparison
const createDeepEqualSelector = createSelectorCreator(
defaultMemoize,
isEqual
);
// Use when inputs are objects/arrays that might be recreated
const selectComplexData = createDeepEqualSelector(
[
state => state.filters,
state => state.items
],
(filters, items) => {
// Even if filters object is recreated with same values,
// this won't recompute
return items.filter(item =>
Object.entries(filters).every(([key, value]) =>
item[key] === value
)
);
}
);
// Custom cache size
const createSelectorWithCacheSize = createSelectorCreator(
defaultMemoize,
undefined,
{ maxSize: 10 } // Cache last 10 results
);
// Useful for selectors with frequently changing parameters
const selectItemsByCategory = createSelectorWithCacheSize(
[
state => state.items,
(state, category) => category
],
(items, category) => items.filter(item => item.category === category)
);
Real-World Example: E-commerce Application
// selectors/index.js
import { createSelector } from 'reselect';
// Base selectors
const selectProducts = state => state.products.items;
const selectProductsLoading = state => state.products.loading;
const selectCart = state => state.cart;
const selectUser = state => state.user;
const selectFilters = state => state.filters;
// Product selectors
export const selectFilteredProducts = createSelector(
[selectProducts, selectFilters],
(products, filters) => {
let filtered = products;
if (filters.category) {
filtered = filtered.filter(p => p.category === filters.category);
}
if (filters.priceRange) {
const [min, max] = filters.priceRange;
filtered = filtered.filter(p => p.price >= min && p.price <= max);
}
if (filters.searchTerm) {
const term = filters.searchTerm.toLowerCase();
filtered = filtered.filter(p =>
p.name.toLowerCase().includes(term) ||
p.description.toLowerCase().includes(term)
);
}
if (filters.inStock) {
filtered = filtered.filter(p => p.stock > 0);
}
return filtered;
}
);
export const selectSortedProducts = createSelector(
[selectFilteredProducts, selectFilters],
(products, filters) => {
const sorted = [...products];
switch (filters.sortBy) {
case 'price-low':
return sorted.sort((a, b) => a.price - b.price);
case 'price-high':
return sorted.sort((a, b) => b.price - a.price);
case 'name':
return sorted.sort((a, b) => a.name.localeCompare(b.name));
case 'rating':
return sorted.sort((a, b) => b.rating - a.rating);
default:
return sorted;
}
}
);
// Cart selectors
export const selectCartItems = createSelector(
[selectCart, selectProducts],
(cart, products) => {
return cart.items.map(cartItem => ({
...cartItem,
product: products.find(p => p.id === cartItem.productId),
subtotal: cartItem.quantity * products.find(p => p.id === cartItem.productId)?.price || 0
}));
}
);
export const selectCartTotals = createSelector(
[selectCartItems, selectUser],
(items, user) => {
const subtotal = items.reduce((sum, item) => sum + item.subtotal, 0);
const taxRate = user?.location?.taxRate || 0.08; // 8% default
const tax = subtotal * taxRate;
const shipping = subtotal > 100 ? 0 : 10; // Free shipping over $100
const discount = user?.membership === 'premium' ? subtotal * 0.1 : 0;
const total = subtotal + tax + shipping - discount;
return {
subtotal,
tax,
shipping,
discount,
total,
itemCount: items.reduce((sum, item) => sum + item.quantity, 0)
};
}
);
// Checkout selectors
export const selectCheckoutData = createSelector(
[selectCartItems, selectCartTotals, selectUser],
(items, totals, user) => ({
items,
totals,
shippingAddress: user?.addresses?.find(a => a.isDefault),
paymentMethod: user?.paymentMethods?.find(p => p.isDefault),
canCheckout: items.length > 0 && totals.total > 0
})
);
// Dashboard selectors
export const selectDashboardStats = createSelector(
[selectProducts, selectUser, selectCart],
(products, user, cart) => ({
totalProducts: products.length,
inStockProducts: products.filter(p => p.stock > 0).length,
lowStockProducts: products.filter(p => p.stock > 0 && p.stock < 10).length,
cartValue: cart.items.reduce((sum, item) =>
sum + (item.quantity * products.find(p => p.id === item.productId)?.price || 0)
, 0),
savedItems: user?.savedItems?.length || 0,
recentOrders: user?.orders?.slice(0, 5) || []
})
);
// Component usage
const ProductList = () => {
const products = useSelector(selectSortedProducts);
const loading = useSelector(selectProductsLoading);
if (loading) return <LoadingSpinner />;
return (
<div className="product-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
};
const ShoppingCart = () => {
const items = useSelector(selectCartItems);
const totals = useSelector(selectCartTotals);
return (
<div className="shopping-cart">
<h2>Shopping Cart ({totals.itemCount} items)</h2>
{items.map(item => (
<CartItem key={item.id} item={item} />
))}
<div className="cart-totals">
<div>Subtotal: ${totals.subtotal.toFixed(2)}</div>
<div>Tax: ${totals.tax.toFixed(2)}</div>
<div>Shipping: ${totals.shipping.toFixed(2)}</div>
{totals.discount > 0 && (
<div>Discount: -${totals.discount.toFixed(2)}</div>
)}
<div>Total: ${totals.total.toFixed(2)}</div>
</div>
</div>
);
};
const CheckoutPage = () => {
const checkoutData = useSelector(selectCheckoutData);
if (!checkoutData.canCheckout) {
return <EmptyCart />;
}
return (
<div className="checkout">
<CheckoutItems items={checkoutData.items} />
<ShippingAddress address={checkoutData.shippingAddress} />
<PaymentMethod method={checkoutData.paymentMethod} />
<OrderSummary totals={checkoutData.totals} />
</div>
);
};
Testing Selectors
// selectors.test.js
import {
selectVisibleTodos,
selectTodoStats,
selectFilteredProducts,
selectCartTotals
} from './selectors';
describe('Todo Selectors', () => {
const state = {
todos: [
{ id: 1, text: 'Learn Redux', completed: true },
{ id: 2, text: 'Learn Reselect', completed: false },
{ id: 3, text: 'Build App', completed: false }
],
filter: 'SHOW_ALL'
};
test('selectVisibleTodos returns all todos when filter is SHOW_ALL', () => {
const result = selectVisibleTodos(state);
expect(result).toHaveLength(3);
});
test('selectVisibleTodos filters completed todos', () => {
const stateWithFilter = { ...state, filter: 'SHOW_COMPLETED' };
const result = selectVisibleTodos(stateWithFilter);
expect(result).toHaveLength(1);
expect(result[0].text).toBe('Learn Redux');
});
test('selectTodoStats calculates correct statistics', () => {
const stats = selectTodoStats(state);
expect(stats).toEqual({
total: 3,
completed: 1,
active: 2,
percentComplete: 33
});
});
test('selector memoization works correctly', () => {
// First call
const result1 = selectVisibleTodos(state);
// Second call with same state
const result2 = selectVisibleTodos(state);
// Should return same reference (memoized)
expect(result1).toBe(result2);
// Change state
const newState = { ...state, filter: 'SHOW_ACTIVE' };
const result3 = selectVisibleTodos(newState);
// Should return new reference
expect(result3).not.toBe(result1);
});
});
describe('E-commerce Selectors', () => {
const state = {
products: {
items: [
{ id: 1, name: 'Laptop', price: 999, category: 'electronics' },
{ id: 2, name: 'Phone', price: 699, category: 'electronics' },
{ id: 3, name: 'Book', price: 29, category: 'books' }
]
},
cart: {
items: [
{ id: 1, productId: 1, quantity: 1 },
{ id: 2, productId: 3, quantity: 2 }
]
},
filters: {
category: 'electronics',
priceRange: [500, 1000]
},
user: {
membership: 'premium',
location: { taxRate: 0.08 }
}
};
test('selectFilteredProducts applies filters correctly', () => {
const filtered = selectFilteredProducts(state);
expect(filtered).toHaveLength(2);
expect(filtered.every(p => p.category === 'electronics')).toBe(true);
});
test('selectCartTotals calculates correct totals', () => {
const totals = selectCartTotals(state);
expect(totals.subtotal).toBe(1057); // 999 + 29*2
expect(totals.discount).toBe(105.7); // 10% premium discount
expect(totals.shipping).toBe(0); // Free shipping over $100
});
});
// Performance testing
describe('Selector Performance', () => {
test('expensive computation is memoized', () => {
const mockExpensiveComputation = jest.fn((items) => {
// Simulate expensive operation
return items.reduce((acc, item) => acc + item.value, 0);
});
const selectExpensiveResult = createSelector(
state => state.items,
mockExpensiveComputation
);
const state = {
items: [{ value: 1 }, { value: 2 }, { value: 3 }]
};
// First call
selectExpensiveResult(state);
expect(mockExpensiveComputation).toHaveBeenCalledTimes(1);
// Second call with same state
selectExpensiveResult(state);
expect(mockExpensiveComputation).toHaveBeenCalledTimes(1); // Still 1
// Change state
const newState = {
items: [{ value: 4 }, { value: 5 }]
};
selectExpensiveResult(newState);
expect(mockExpensiveComputation).toHaveBeenCalledTimes(2);
});
});
Performance Optimization Tips
Do's
- Use createSelector for computed values
- Keep input selectors simple and focused
- Compose selectors for complex computations
- Use selector factories for parameterized selectors
- Profile selector performance in development
- Test selector memoization
Don'ts
- Don't create new objects/arrays in input selectors
- Don't use inline selectors with complex logic
- Don't create selector instances on every render
- Don't over-memoize simple selectors
- Don't ignore selector recomputation warnings
Performance Patterns
// ❌ Bad: Creates new array every time
const selectUserIds = state => state.users.map(user => user.id);
// ✅ Good: Memoized computation
const selectUserIds = createSelector(
state => state.users,
users => users.map(user => user.id)
);
// ❌ Bad: Inline selector with computation
const Component = () => {
const expensiveData = useSelector(state =>
state.items.filter(item => item.active).map(item => ({
...item,
computed: heavyComputation(item)
}))
);
};
// ✅ Good: Extracted and memoized
const selectExpensiveData = createSelector(
state => state.items,
items => items
.filter(item => item.active)
.map(item => ({
...item,
computed: heavyComputation(item)
}))
);
const Component = () => {
const expensiveData = useSelector(selectExpensiveData);
};
// ❌ Bad: Creating selector on every render
const Component = ({ category }) => {
const items = useSelector(
createSelector(
state => state.items,
items => items.filter(item => item.category === category)
)
);
};
// ✅ Good: Memoized selector instance
const Component = ({ category }) => {
const selectItemsByCategory = useMemo(
() => createSelector(
state => state.items,
(state, category) => category,
(items, category) => items.filter(item => item.category === category)
),
[]
);
const items = useSelector(state => selectItemsByCategory(state, category));
};
Practice Exercise
Task: Create an Advanced Product Filter System
Build a comprehensive selector system for an e-commerce product filter with:
- Multiple filter criteria (category, price, rating, etc.)
- Search functionality
- Sorting options
- Pagination
- Performance optimization
// TODO: Create the selector system
import { createSelector } from 'reselect';
// Base selectors
const selectProducts = state => state.products;
const selectFilters = state => state.filters;
const selectSearchTerm = state => state.search.term;
const selectSortBy = state => state.sort.by;
const selectSortDirection = state => state.sort.direction;
const selectCurrentPage = state => state.pagination.currentPage;
const selectItemsPerPage = state => state.pagination.itemsPerPage;
// TODO: Create filtered products selector
export const selectFilteredProducts = createSelector(
// Implement filtering logic
);
// TODO: Create searched products selector
export const selectSearchedProducts = createSelector(
// Implement search logic
);
// TODO: Create sorted products selector
export const selectSortedProducts = createSelector(
// Implement sorting logic
);
// TODO: Create paginated products selector
export const selectPaginatedProducts = createSelector(
// Implement pagination logic
);
// TODO: Create stats selector
export const selectProductStats = createSelector(
// Calculate stats like total count, price range, etc.
);
// TODO: Create facets selector for filter options
export const selectAvailableFilters = createSelector(
// Generate available filter options based on products
);
// Example component using the selectors
const ProductList = () => {
const products = useSelector(selectPaginatedProducts);
const stats = useSelector(selectProductStats);
const filters = useSelector(selectAvailableFilters);
// Implement component
};