Selectors and Reselect

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
};
                

Additional Resources