In Redux, what is a selector function, and why do components call selectors instead of reading state paths directly?
answer
- a function of the root state
- components stop knowing the shape
- one place to change on refactor
- keep stored state minimal
basics
~20 sA Redux selector is a function of the root state, plus optional arguments, returning part of it or a value derived from it. Using selectors keeps knowledge of the state shape in one place, so reshaping state changes selectors, not components.
solid answer
~40 sA selector is any function `(state: RootState, ...args) => value`, such as `selectCartItems = (state) => state.cart.items`. Components pass selectors to `useSelector`, and the same selectors are reused in thunks via `getState()`, in middleware and in tests. The main benefit is **encapsulation**: the slice file that owns `state.cart` also exports the selectors for it, so when the shape changes — items moved under `state.cart.byId`, say — you update a few selectors instead of every component that wrote `state.cart.items`. Selectors also let you keep the stored state **minimal**: totals, counts and filtered lists are computed by selectors from the real data instead of being stored and kept in sync by hand. A plain selector does no caching; memoisation is a separate step you add when a selector returns a newly built value.
code
ts · 12 linesimport type { RootState, AppThunk } from './store'
import { checkoutStarted } from './checkoutSlice'
export const selectCartItems = (state: RootState) => state.cart.items
export const selectCartTotal = (state: RootState) =>
selectCartItems(state).reduce((sum, i) => sum + i.price * i.quantity, 0)
export const startCheckout = (): AppThunk => (dispatch, getState) => {
if (selectCartItems(getState()).length === 0) return
dispatch(checkoutStarted(selectCartTotal(getState())))
}go deeper
Recall that a selector is a plain function of the root state, named selectSomething, used with useSelector and reusable anywhere you have the state.
Explain encapsulation of the state shape with a concrete refactor, and why derived values are computed by selectors rather than stored in state.
Organise selectors per slice, typed against RootState, and recognise which selectors return new references and therefore need memoisation.
Treat selectors as the read API of each slice, which lets teams reshape state or split slices without touching consuming features.
## What a selector is In Redux, a **selector** is a function that receives the store's root state, optionally extra arguments, and returns some value from it: ```ts export const selectCartItems = (state: RootState) => state.cart.items export const selectCartItemById = (state: RootState, id: string) => state.cart.items.find((item) => item.id === id) ``` There is nothing special about the function: no library is needed to write one, and by convention its name starts with `select`. What makes it a selector is its role — the **only** code that knows where a value lives in the state tree. ## Where selectors are used The same selector can be called from every place that has access to the state: - in components, passed to React-Redux's `useSelector(selectCartItems)`; - in thunks, as `selectCartItems(getState())`, to read state before deciding what to dispatch; - in middleware and listeners, which also receive `getState`; - in unit tests, called directly with a hand-built state object; - as input selectors for memoised selectors built with Reselect's `createSelector`. Because they are plain functions of state, selectors are the easiest part of a Redux app to test. ## Why not read state paths directly Writing `useSelector((state) => state.cart.items)` inline in thirty components works until the state is reshaped. Common reshapes include: 1. normalising an array into `{ ids, entities }`; 2. moving a slice under a new key, such as `state.shop.cart`; 3. renaming a field. With inline paths, each change means finding and editing every component. With selectors exported from the slice file, the change is local: the slice's selectors are updated in the same commit as its reducer, and components keep calling `selectCartItems`. | | Inline path in components | Selector exported by the slice | |---|---|---| | Knows the state shape | every component | the slice file only | | Reshaping state | edit all readers | edit the selectors | | Reuse in thunks and tests | copy the path again | call the same function | | Derived values | recomputed ad hoc in each component | computed once, in one named place | ## Keep stored state minimal, derive the rest Redux's style guidance is to store the **minimal** state and derive everything else. A cart stores items with prices and quantities; it does not store `itemCount` or `totalPrice`, because those can be computed: ```ts export const selectCartTotal = (state: RootState) => state.cart.items.reduce((sum, i) => sum + i.price * i.quantity, 0) ``` Storing the total as well would mean every reducer that touches items must also update it, and a missed case leaves the two out of sync. A selector cannot drift from the data it reads. ## Organising selectors - **Colocate** selectors with the slice that owns the state, in the same file or a neighbouring one. - **Type** them against `RootState`, inferred from the store with `ReturnType<typeof store.getState>`. - Keep them **pure**: a selector reads state and arguments only, with no side effects. - Name derived selectors after what they return, such as `selectCartTotal`, not after how they compute it. ## Testing selectors Because a selector is a pure function of state, testing it needs no store and no React: ```ts const state = { cart: { items: [{ id: 'a', price: 10, quantity: 3 }] } } as RootState expect(selectCartTotal(state)).toBe(30) ``` Composing selectors on each other also pays off here. `selectCartTotal` calls `selectCartItems` rather than reading `state.cart.items` itself, so the path appears in exactly one function. When the cart is normalised later, only the base selector changes, and the tests for derived selectors keep passing unchanged. Base selectors read paths; derived selectors call base selectors. ## What a plain selector does not do A plain selector runs its body on every call. When it only reads a path, that is cheap and returns an existing reference. When it **builds** a new array or object — `filter`, `map`, a spread — every call returns a new reference even if nothing changed. That is when you reach for a memoised selector created with Reselect's `createSelector`, which returns the cached result until its inputs change. An interview answer should cover the definition, the encapsulation argument with a concrete reshaping example, the minimal-state principle, and the distinction between a plain selector and a memoised one.
- Should a Redux cart slice store totalPrice alongside its items?No. The total can be computed from the items, so storing it creates a second source of truth that every reducer touching items must keep in sync. Store the items and derive the total with a selector such as `selectCartTotal`; memoise it only if computing it becomes measurably expensive or it returns a new reference.
- Do plain Redux selectors reduce re-renders?Not by themselves. A plain selector runs its body on every call. If it returns an existing piece of state, the reference is stable anyway; if it builds a new array or object, every call yields a new reference. Stability for built values comes from memoising the selector with `createSelector`.
saying these in an interview costs you the question
- A selector must be created with createSelector to count as one
- Selectors are only usable inside React components
- Derived totals should be stored in state for speed
- A plain selector caches its result between calls
- Selectors belong in the component file that uses them