Learn Redux in 10 DaysDay 8: Selectors & Performance
Chapter 8Learn Redux in 10 Days

Day 8: Selectors & Performance

What You'll Learn Today

  • What selectors are and why they matter
  • Simple selectors vs memoized selectors
  • How createSelector and memoization work
  • Composing selectors for complex data
  • useSelector and re-render behavior
  • Common performance pitfalls and how to avoid them
  • createEntityAdapter selectors
  • 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 useSelector to fetch its own data. This way, when one todo changes, only that TodoItem re-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

  1. Open the Profiler tab in React DevTools
  2. Enable Settings > Highlight updates when components render
  3. 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:

  1. Selectors serve as an abstraction layer over your state shape
  2. Memoize operations that create new references (filter(), map(), object literals) with createSelector
  3. Never return new objects or arrays from inline selectors passed to useSelector
  4. For large lists, use the "ID list + individual fetch" pattern
  5. 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.