Skip to the content.
Agent Skill Available Download this Agent Skill (SKILL.md) to drop into ~/.claude/skills/ or any Agent-Skills-compatible runtime for AI-assisted Selectors work.
Download
selectors

Selectors

Key Insight

Selectors are the “computed properties” of Redux—they transform raw state into the exact shape your components need, and memoization ensures expensive computations only run when their inputs actually change. Without selectors, every component re-render triggers full data transformations (filtering, sorting, aggregating), but with memoized selectors like Reselect, a 10,000-item list gets filtered once and cached until the list changes. Selectors also future-proof your code: when you refactor state structure, you update selectors, not every component.

Detailed Description

Selectors in state management are pure functions that extract and compute derived data from the application’s state. They act as the “read” layer between raw Redux state and React components, transforming state into component-friendly formats without modifying the state itself.

The selector pattern emerged to solve several problems: components were duplicating transformation logic, expensive computations ran on every render, and state shape changes required updating dozens of components. Selectors centralize this logic, add memoization for performance, and create a clean API layer over state.

Key aspects of selectors:

Code Examples

Basic Example: getVisibleTodos across state libraries

The same derived selector — give me the filtered set of visible todos — across four state libraries. Classic Redux, Redux Saga, and WC-Saga all use the same plain-function selector (Saga templates share this exact file). Pinia exposes it as a getter on the store. NgRx composes memoized selectors via createSelector.

Classic Redux

// templates/chota-react-redux/src/state/todo/todo.selectors.js
// Plain selector function. The React-Saga and WC-Saga templates ship the
// same file — the selector pattern is independent of the middleware.
import { SHOW_ACTIVE, SHOW_ALL, SHOW_COMPLETED } from "../filters/filters.type";

export const getVisibleTodos = (todo, filter) => {
  let visibleTodos = [];
  switch (filter) {
    case SHOW_ALL:       visibleTodos = todo.todoItems; break;
    case SHOW_COMPLETED: visibleTodos = todo.todoItems.filter((t) => t.completed); break;
    case SHOW_ACTIVE:    visibleTodos = todo.todoItems.filter((t) => !t.completed); break;
    default:             throw new Error("Unknown filter: " + filter);
  }
  return { ...todo, todoItems: visibleTodos };
};

Redux Toolkit

// templates/chota-react-rtk/src/state/todo/todo.selectors.js
// RTK doesn't add a selector layer of its own for the basic case — you
// write a plain selector the same way. Reach for `createSelector`
// (re-exported from RTK) when you need memoization for expensive shapes.
import { SHOW_ACTIVE, SHOW_ALL, SHOW_COMPLETED } from "../filters/filters.type";

export const getVisibleTodos = (todo, filter) => {
  switch (filter) {
    case SHOW_ALL:       return { ...todo };
    case SHOW_COMPLETED: return { ...todo, todoItems: todo.todoItems.filter((t) => t.completed) };
    case SHOW_ACTIVE:    return { ...todo, todoItems: todo.todoItems.filter((t) => !t.completed) };
    default:             return todo;
  }
};

// With memoization:
// export const getVisibleTodos = createSelector(
//   [(state) => state.todo, (state) => state.filters],
//   (todo, filters) => { /* ...same logic... */ }
// );

NgRx

// templates/chota-angular-ngrx/src/app/state/todo/todo.selectors.ts
// createSelector composes input selectors into a memoized output selector.
// The combiner only re-runs when an input selector returns a new reference.
import { createSelector } from '@ngrx/store';
import { AppState } from '../index';
import { getSelectedFilter } from '../filters/filters.selectors';

export const getTodoState = (state: AppState) => state.todo;

export const getVisibleTodos = createSelector(
  getTodoState,
  getSelectedFilter,
  (todoState, selectedFilter) => {
    const { todoItems } = todoState;
    switch (selectedFilter?.id) {
      case 'SHOW_COMPLETED':
        return { ...todoState, todoItems: todoItems.filter((t) => t.completed) };
      case 'SHOW_ACTIVE':
        return { ...todoState, todoItems: todoItems.filter((t) => !t.completed) };
      default:
        return todoState;
    }
  }
);

Pinia

// templates/chota-vue-pinia/src/state/todo/index.js
// Pinia "selectors" are getters on the store — functions of state that
// Vue's reactivity system caches automatically. The selector function
// itself lives in todo.selectors.js and is reused from the getter.
import { defineStore } from 'pinia';
import { getVisibleTodos } from './todo.selectors';
import { useFiltersStore } from '../filters';
import { getSelectedFilter } from '../filters/filters.selectors';

export const useTodoStore = defineStore('todo', {
  state: () => ({ /* ...initialTodoState */ }),
  getters: {
    visibleTodos(state) {
      const filtersData = useFiltersStore();
      const selectedFilter = getSelectedFilter(filtersData);
      return getVisibleTodos(state, selectedFilter.id);
    },
  },
});

// templates/chota-vue-pinia/src/state/todo/todo.selectors.js
export const getVisibleTodos = (todo, filter) => {
  /* same switch on filter as the Redux tab */
};

The takeaway: the derivation — “apply the active filter to the todo list” — is the same pure function everywhere. What changes is how each library wires memoization and subscription. Redux-family templates use plain functions and optionally opt into createSelector. NgRx uses createSelector as the default path so every output selector is memoized. Pinia uses Vue’s reactivity (a getter on the store) and reuses the same plain selector internally.

Practical Example: Memoized Selectors with Reselect

Once a selector starts combining multiple state slices or doing real work (filter + search + count), opt into createSelector. Each input selector is a tiny state-reader; the combiner only re-runs when one of those inputs returns a new reference.

import { createSelector } from 'reselect';

// Input selectors (simple extraction)
const selectTodos = state => state.todos.items;
const selectFilter = state => state.todos.filter;
const selectSearchTerm = state => state.todos.searchTerm;

// Memoized derived selector
const selectFilteredTodos = createSelector(
  [selectTodos, selectFilter, selectSearchTerm],
  (todos, filter, searchTerm) => {
    // Expensive computation - only runs when inputs change
    let filtered = todos;
    
    // Filter by completion status
    if (filter === 'active') {
      filtered = filtered.filter(t => !t.completed);
    } else if (filter === 'completed') {
      filtered = filtered.filter(t => t.completed);
    }
    
    // Filter by search term
    if (searchTerm) {
      filtered = filtered.filter(t =>
        t.text.toLowerCase().includes(searchTerm.toLowerCase())
      );
    }
    
    return filtered;
  }
);

// Composed selectors
const selectTodoById = createSelector(
  [selectTodos, (_, todoId) => todoId],
  (todos, todoId) => todos.find(t => t.id === todoId)
);

const selectCompletedCount = createSelector(
  [selectTodos],
  (todos) => todos.filter(t => t.completed).length
);

const selectTodoStats = createSelector(
  [selectTodos],
  (todos) => ({
    total: todos.length,
    completed: todos.filter(t => t.completed).length,
    active: todos.filter(t => !t.completed).length
  })
);

// Usage
function TodoStats() {
  const stats = useSelector(selectTodoStats);
  
  return (
    <div>
      Total: {stats.total} | Active: {stats.active} | Done: {stats.completed}
    </div>
  );
}

Advanced Example: Parametrized and Normalized Selectors

Real apps store entities in { byId, allIds } shape so updates stay O(1) and references stay stable. Selectors then become the join layer — denormalizing on read, parametrized by id, with per-instance caches when the same selector is called with different props.

import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect';

// Normalized state structure
const state = {
  users: {
    byId: {
      '1': { id: '1', name: 'Alice', postIds: ['101', '102'] },
      '2': { id: '2', name: 'Bob', postIds: ['103'] }
    },
    allIds: ['1', '2']
  },
  posts: {
    byId: {
      '101': { id: '101', title: 'Post 1', authorId: '1' },
      '102': { id: '102', title: 'Post 2', authorId: '1' },
      '103': { id: '103', title: 'Post 3', authorId: '2' }
    },
    allIds: ['101', '102', '103']
  }
};

// Base selectors
const selectUsersById = state => state.users.byId;
const selectPostsById = state => state.posts.byId;

// Parametrized selector factory
const makeSelectUserById = () =>
  createSelector(
    [selectUsersById, (_, userId) => userId],
    (usersById, userId) => usersById[userId]
  );

// Denormalized selector (joins data)
const makeSelectUserWithPosts = () =>
  createSelector(
    [makeSelectUserById(), selectPostsById],
    (user, postsById) => {
      if (!user) return null;
      
      return {
        ...user,
        posts: user.postIds.map(id => postsById[id])
      };
    }
  );

// Custom equality check for deep comparisons
const createDeepEqualSelector = createSelectorCreator(
  defaultMemoize,
  (a, b) => JSON.stringify(a) === JSON.stringify(b)
);

const selectFilteredUsers = createDeepEqualSelector(
  [selectUsersById, (_, filter) => filter],
  (usersById, filter) => {
    return Object.values(usersById).filter(user =>
      user.name.includes(filter.name) &&
      user.age > filter.minAge
    );
  }
);

// Usage with React
function UserProfile({ userId }) {
  // Create selector instance per component instance
  const selectUserWithPosts = useMemo(makeSelectUserWithPosts, []);
  
  const user = useSelector(state => selectUserWithPosts(state, userId));
  
  return (
    <div>
      <h1>{user.name}</h1>
      <ul>
        {user.posts.map(post => <li key={post.id}>{post.title}</li>)}
      </ul>
    </div>
  );
}

Common Mistakes

1. Creating New Objects in Selectors Without Memoization

Mistake: Returning new object references on every call.

// ❌ BAD: New array created every call
const selectActiveTodos = (state) => {
  return state.todos.filter(t => !t.completed);  // New array every time
};

// Component re-renders infinitely
function TodoList() {
  const todos = useSelector(selectActiveTodos);
  // todos is new array every render → triggers re-render → infinite loop
}
// ✅ GOOD: Memoized selector
import { createSelector } from 'reselect';

const selectTodos = state => state.todos;

const selectActiveTodos = createSelector(
  [selectTodos],
  (todos) => todos.filter(t => !t.completed)
  // Only recomputes when todos array changes
);

Why it matters: Non-memoized selectors returning new objects cause unnecessary re-renders. Reselect memoization prevents this.

2. Sharing Selector Instances Across Components

Mistake: Using single parametrized selector for multiple component instances.

// ❌ BAD: Shared selector instance
const selectUserById = createSelector(
  [state => state.users, (_, id) => id],
  (users, id) => users.find(u => u.id === id)
);

function UserList({ userIds }) {
  return userIds.map(id => <UserCard key={id} userId={id} />);
}

function UserCard({ userId }) {
  const user = useSelector(state => selectUserById(state, userId));
  // Selector cache thrashes - different userId each component
  return <div>{user.name}</div>;
}
// ✅ GOOD: Selector factory
const makeSelectUserById = () =>
  createSelector(
    [state => state.users, (_, id) => id],
    (users, id) => users.find(u => u.id === id)
  );

function UserCard({ userId }) {
  const selectUserById = useMemo(makeSelectUserById, []);
  const user = useSelector(state => selectUserById(state, userId));
  return <div>{user.name}</div>;
}

Why it matters: Shared parametrized selectors lose memoization benefits. Create selector instance per component instance.

3. Putting Non-Serializable Data in Selectors

Mistake: Returning functions, class instances, or other non-serializable data.

// ❌ BAD: Non-serializable result
const selectTodoHandlers = createSelector(
  [selectTodos],
  (todos) => ({
    todos,
    addTodo: (text) => dispatch(addTodo(text)),  // Function!
    deleteTodo: (id) => dispatch(deleteTodo(id))
  })
);
// ✅ GOOD: Return data only
const selectTodos = state => state.todos;

// Handlers in component
function TodoList() {
  const todos = useSelector(selectTodos);
  const dispatch = useDispatch();
  
  const handleAdd = useCallback((text) => {
    dispatch(addTodo(text));
  }, [dispatch]);
  
  return <Todos todos={todos} onAdd={handleAdd} />;
}

Why it matters: Selectors should return serializable data. Functions belong in components or hooks.

Quick Quiz

What problem does memoization (e.g. via Reselect/createSelector) solve in selectors?

How do you compose selectors?

When do you need a selector factory (createSelector inside a creator fn) vs a shared selector?

How do selectors help decouple components from the store shape?

What's the difference between input selectors and output selectors in Reselect?

References