Selectors (Saga template)
What is a Selector?
A selector is a pure function (state) => derivedValue that hides the state shape from consumers. Containers call selectors with useSelector(selector); components only see the projected view-data, never the slice's internal flags or normalisation tricks.
Key Principles
Colocate selectors with their slice.
<slice>.selectors.jslives next to<slice>.reducer.js. Cross-slice selectors live where the primary slice lives (e.g.getVisibleTodoslives intodo.selectors.jsbecause that's where the items list lives, even though it also readsfilters).One selector per derived shape. A selector returns either a primitive (
isLoading), a slice ref (state.todo), a list (state.todo.todoItems), or a derived projection (getVisibleTodos). Don't fuse unrelated derivations into one selector.Pass-through selectors are not boilerplate, they're API.
getTodoItems = (state) => state.todo.todoItemslooks redundant but it's the contract: tomorrow you can normalise the slice or rename the field, and only this file changes.Cross-slice selectors take both slices as args. The
getVisibleTodos(todoSlice, filterId)pattern keeps the selector pure and easily testable; the container picks the slices viauseSelectorand passes them in.Memoise only when measured. Plain selectors are cheap. Reach for
createSelector(reselect) when a selector returns a NEW array/object on every call AND that result is consumed by a memoised component — otherwise you'll see needless re-renders.
Best Practices
✅ DO:
- Name boolean flag selectors
is…(isLoading,isContentLoading). - Name list selectors
get<Plural>and projectionsget<Verb><Plural>(e.g.getVisibleTodos). - Keep selectors stateless: no module-level caches; if you need memoisation, use reselect.
❌ DON'T:
- Don't fetch inside selectors. Sagas do that.
- Don't read
localStorageorDate.now()from a selector. Tests will fail nondeterministically. - Don't return
stateitself — projects always start one level in (state.todo).
Code Patterns
Slice selectors (todo.selectors.js)
// Saga state shape:
// { isLoading, isActionLoading, isContentLoading, error,
// todoItems[], currentTodoItem, previousStateTodoItems? }
// Pass-through projections — the API for downstream consumers.
export const getTodo = (state) => state.todo;
export const getTodoItems = (state) => state.todo.todoItems;
export const getCurrentTodoItem = (state) => state.todo.currentTodoItem;
// Lifecycle flags (the UI picks one to render against).
export const isContentLoading = (state) => state.todo.isContentLoading;
export const isActionLoading = (state) => state.todo.isActionLoading;
export const getError = (state) => state.todo.error;
// Cross-slice: takes the slice + the filter id, returns the view list.
// (the container picks both via useSelector and calls this in render).
export const getVisibleTodos = (todoSlice, filterId) => {
const items = todoSlice.todoItems;
switch (filterId) {
case "SHOW_COMPLETED": return items.filter((t) => t.completed);
case "SHOW_ACTIVE": return items.filter((t) => !t.completed);
case "SHOW_ALL":
default: return items;
}
};
Filter slice selectors (filters.selectors.js)
// The filter slice is an array of { id, label, selected }.
export const getSelectedFilter = (state) =>
state.filters.find((f) => f.selected) || state.filters[0];
export const getFilters = (state) => state.filters;
Calling from a container
import { useSelector } from "react-redux";
import { getSelectedFilter } from "../state/filters/filters.selectors";
import { getVisibleTodos } from "../state/todo/todo.selectors";
const selectedFilter = useSelector(getSelectedFilter);
const todoData = useSelector((state) =>
getVisibleTodos(state.todo, selectedFilter.id)
);
When to add createSelector
// Reach for reselect when a derived projection is hot AND consumed by
// a memoised component. Most slices don't need this.
import { createSelector } from "reselect";
const selectTodo = (state) => state.todo;
const selectFilter = (state, filterId) => filterId;
export const makeGetVisibleTodos = createSelector(
[selectTodo, selectFilter],
(todoSlice, filterId) => /* …same logic as above… */
);
Related Terminologies
- Reducer — owns the state shape selectors project from.
- Container — the only call-site of
useSelector(selector). - Filters — the cross-slice friend of
getVisibleTodos.
Quality Gates
- One selectors file per slice; no mixing.
- Pass-through selectors exist for every consumed field.
- No side effects (no fetch, no
Math.random, noDate.now). - Cross-slice selectors take slice args, not
state. -
createSelectoronly used where memoisation is measurably needed.
Source: templates/chota-react-saga/src/state/todo/todo.selectors.js, filters.selectors.js