In NgRx, what does an entity adapter's getSelectors() return, and why pass it a selector for the board's feature state?
answer
- four ready-made selectors
- no argument: slice-level functions
- root state has no ids
- parent selector makes them memoized
- selectAll maps ids to entities
basics
~10 sgetSelectors() returns selectIds, selectEntities, selectAll and selectTotal. With no argument they read an EntityState directly; given a selector for the feature slice, they become memoized selectors that start from the root state.
solid answer
~30 s`cardAdapter.getSelectors()` returns `selectIds`, `selectEntities`, `selectAll` (ids mapped to records, in order) and `selectTotal` (the count). With no argument they expect the entity slice itself, which works inside a `createSelector` that already narrowed to the board, but `store.select(cardAdapter.getSelectors().selectAll)` hands them the root state, where `ids` is `undefined`, and it fails. `cardAdapter.getSelectors(selectBoardState)` instead returns four `MemoizedSelector`s built with `createSelector` from that parent, safe to pass to `store.select` or `selectSignal`. `selectAll` recomputes only when `ids` or `entities` changes reference, so switching `selectedCardKey` returns the same array. There is no by-key selector; combine `selectEntities` with the selected key.
code
ts · 25 linesimport { createFeatureSelector, createSelector } from '@ngrx/store';
import { BoardState, cardAdapter } from './board.state';
export const selectBoardState = createFeatureSelector<BoardState>('board');
export const {
selectAll: selectAllCards,
selectEntities: selectCardEntities,
selectTotal: selectCardCount,
} = cardAdapter.getSelectors(selectBoardState);
const selectSelectedKey = createSelector(
selectBoardState,
(board) => board.selectedCardKey
);
export const selectSelectedCard = createSelector(
selectCardEntities,
selectSelectedKey,
(entities, key) => (key ? entities[key] : undefined)
);
export const selectDoneCards = createSelector(selectAllCards, (cards) =>
cards.filter((card) => card.columnId === 'done')
);go deeper
Name the four selectors getSelectors returns and what each gives you.
Explain the two overloads: slice-level functions without an argument, memoized root-level selectors with a parent selector, and the failure when the first meets store.select.
Use the reference behaviour of selectAll to reason about re-renders, and compose by-key and per-column selectors from the adapter's outputs.
Standardise where adapter selectors are built and named so features expose one consistent read surface instead of ad hoc lookups.
## What getSelectors gives you Every adapter made by `createEntityAdapter` carries a `getSelectors` method that builds the four reads almost every collection needs: - **`selectIds`** — the `ids` array, in the adapter's order. - **`selectEntities`** — the `entities` dictionary, for lookup by key. - **`selectAll`** — `ids` mapped to their records: the collection as an ordered array. - **`selectTotal`** — `ids.length`, the number of records. There is no selector for one record by key. That is the job of a selector you compose from `selectEntities` and whatever holds the key. ## The two overloads `getSelectors` has two call forms, and the difference is the source of the most common bug with it. | Call | Input each selector expects | Built with | Safe for `store.select` | |---|---|---|---| | `getSelectors()` | the `EntityState` slice itself | plain functions plus memoized `selectAll`/`selectTotal` | no | | `getSelectors(selectBoardState)` | the root state | `createSelector` from the parent | yes | With **no argument**, `selectIds` is simply `(state) => state.ids`. That is right when the caller already holds the board slice: inside a reducer file, or as the projector of a `createSelector` that starts from the slice, which is how the official guide wires them. With a **parent selector**, each of the four is wrapped as `createSelector(selectBoardState, ...)`, so it accepts the root state and is typed as a `MemoizedSelector`. The return types name the difference too. The no-argument form is typed `EntitySelectors<Card, EntityState<Card>>`: plain functions whose input is the slice. The parent-selector form is typed `MemoizedEntitySelectors<Card, V>`, where `V` is whatever state your parent selector reads, normally the root state. When a teammate asks which one to export from the feature file, the answer is the second, unless the only callers are other selectors that already start from the slice. ## The bug the overloads cause ```ts // Wrong: the store passes the root state, which has no ids this.store.select(cardAdapter.getSelectors().selectAll); ``` 1. `store.select` calls the selector with the **root** state object. 2. The slice-level `selectIds` reads `root.ids`, which is `undefined`. 3. `selectAll`'s projector calls `.map` on `undefined` and throws a `TypeError`. The fix is to pass the feature selector: `cardAdapter.getSelectors(selectBoardState).selectAll`. ## How memoization plays out on the board `selectAll` is built from `selectIds` and `selectEntities`, so it recomputes only when one of those references changes. Combined with how the adapter writes, that gives useful stability: - Changing `selectedCardKey` produces a new board slice, but `ids` and `entities` keep their references, so `selectAll` returns the **same array**. - Renaming a card creates a new `entities` map, so `selectAll` returns a new array, while `selectIds` returns the same one. - A write that changes nothing returns the original state, so nothing recomputes. ## Reading one card by key Because the adapter has no by-key selector, reading "the open card" is a small composition, and there are two common shapes: - **Key held in state.** Store `selectedCardKey` beside the collection and combine a selector for it with `selectEntities`. The result recomputes when either the key or the dictionary changes, which is what the code example below does. - **Key held elsewhere.** When the key comes from the route or from a component input, build a selector factory that takes the key and returns `createSelector(selectCardEntities, (entities) => entities[key])`. Either way the lookup is `entities[key]`, typed `Card | undefined`, so the template or the caller must handle a key that points at nothing: a card that is still loading, was archived, or never existed. ## Counting and grouping `selectTotal` is the cheap count of the whole collection. Counts per column are a derived selector over `selectAll`: 1. Start from `selectAllCards`. 2. Reduce it into a `Record<string, number>` keyed by `columnId`. 3. Export the result as `selectCardCountByColumn`, memoized like any other selector, so the counts recompute only when the collection changes. ## Wiring it up A typical board feature file exports the adapter selectors under domain names and composes the rest: - Destructure and rename: `selectAll: selectAllCards`, `selectTotal: selectCardCount`. - Build the selected card from `selectEntities` and a selector for `selectedCardKey`. - Build per-column lists by filtering `selectAllCards` on `columnId`; with a `sortComparer`, each column is already in order. - When the feature is declared with `createFeature`, spread `cardAdapter.getSelectors(selectBoardState)` into its `extraSelectors`, as the NgRx entity recipe does. Each call to `getSelectors` builds fresh selectors with their own caches, so call it once per feature and export the results rather than calling it inside components.
- Where is the no-argument form of getSelectors the right choice?Wherever the caller already holds the entity slice: inside the reducer file, where the official guide exports them and then composes each with `createSelector(selectUserState, fromUser.selectAllUsers)`, or in a unit test that calls the selector on a hand-built `EntityState`. Never pass the no-argument selectors straight to `store.select`.
- Why call getSelectors once per feature instead of inside each component?Each call builds new selector instances with their own memoization caches. Calling it in a component creates selectors that start cold for every instance and cannot share cached results. Build them once in the feature's selector file and export them.
saying these in an interview costs you the question
- The no-argument getSelectors() result can be passed straight to store.select.
- selectAll sorts the cards each time it runs.
- getSelectors includes a selectById selector for one record.
- Changing selectedCardKey makes selectAll return a new array.