In NgRx 22, what does createFeature's extraSelectors factory receive, and when should a derived orders selector live there rather than in a separate selectors file?
answer
- a factory, not a list
- input is the generated base set
- extras reference each other in the body
- same name replaces the generated one
- cross-feature joins live outside
basics
~20 sThe extraSelectors factory receives the selectors createFeature generated (the feature selector and one per property) and returns more, which are merged into the feature object. It suits selectors derived from that one slice; joins across features belong in a separate file.
solid answer
~40 s`extraSelectors` is a function: `createFeature` calls it with the **base selectors** it generated, `selectOrdersState` plus one per state property, and spreads whatever object it returns into the feature. So `selectPendingOrders` or `selectSelectedOrder`, built with `createSelector` from `selectOrders` and `selectSelectedId`, ship on `ordersFeature` beside the generated ones, fully typed. The factory sees only the base set, so extras that build on each other are declared as constants inside the factory body and returned together. An extra with the same name as a generated selector replaces it without any warning. My rule: selectors that derive only from this slice go in `extraSelectors`; selectors that join the orders slice with another feature, or exist for one page's view model, go in their own file, so the feature does not import its neighbours.
code
ts · 22 linesimport { createFeature, createSelector } from '@ngrx/store';
export const ordersFeature = createFeature({
name: 'orders',
reducer: ordersReducer,
extraSelectors: ({ selectOrders, selectSelectedId }) => {
const selectActiveOrders = createSelector(selectOrders, (orders) =>
orders.filter((o) => !o.archived)
);
const selectPendingCount = createSelector(
selectActiveOrders,
(active) => active.filter((o) => o.status === 'pending').length
);
const selectSelectedOrder = createSelector(
selectOrders,
selectSelectedId,
(orders, id) => orders.find((o) => o.id === id) ?? null
);
return { selectActiveOrders, selectPendingCount, selectSelectedOrder };
},
});
// ordersFeature.selectPendingCount is typed and sits beside the generated selectorsgo deeper
Recall that createFeature accepts an extraSelectors function and that the selectors it returns appear on the feature object next to the generated ones.
Explain that the factory receives only the base selectors, how extras that depend on each other are declared together, and that a same-named extra replaces a generated one.
Show where you draw the line between the feature's own derived selectors and cross-feature or page view-model selectors, and why that line prevents coupling and circular imports.
Treat each feature object as a slice's public reading API and set team conventions for what it may expose, weighing discoverability against a sprawling surface.
## The problem extraSelectors solves `createFeature({ name, reducer })` generates two kinds of **selectors**, the memoized functions that read state out of the NgRx store: - the **feature selector**, `selectOrdersState` for a feature named `orders`; - one **property selector** per top-level key of the initial state: `selectOrders`, `selectSelectedId`, `selectLoading`. Real screens need more: the orders awaiting shipment, the currently selected order object, the count of archived orders. Without `extraSelectors`, those live in a separate `orders.selectors.ts` that imports the generated selectors from the feature file. `extraSelectors` lets the feature carry its derived selectors itself. ## What the factory receives and returns `extraSelectors` is not an object but a **factory function**: 1. `createFeature` builds the base selectors first. 2. It calls `extraSelectors(baseSelectors)`, passing an object containing exactly the feature selector and the property selectors. 3. It spreads the returned object into the feature object, **after** the base selectors. Consequences worth knowing: | Behaviour | What it means in practice | |---|---| | The input is only the base set | an extra cannot receive another extra as an argument | | The result is spread last | an extra named `selectLoading` **replaces** the generated `selectLoading` | | There is no duplicate-name check | the replacement happens silently, with no error or warning | | Values may be selectors or factories | `selectOrderById: (id: string) => createSelector(...)` is accepted | | The return type flows into the feature | `ordersFeature.selectPendingOrders` is fully typed | To make one extra build on another, declare both as constants inside the factory body and return them together: - `const selectActiveOrders = createSelector(selectOrders, ...)`; - `const selectPendingCount = createSelector(selectActiveOrders, ...)`; - `return { selectActiveOrders, selectPendingCount }`. ## When a selector belongs in extraSelectors Put a selector in the feature when it: - derives only from the **orders slice** itself; - is used by more than one component, so it is part of the slice's public reading surface; - should travel with the slice: whoever imports `ordersFeature` gets it. Keep a selector in a separate file when it: - **joins features**, for example orders with a `customers` slice to show customer names; importing `customersFeature` into the orders feature file couples the two slices' modules, and a join that runs in both directions invites a circular import; - builds a **view model** for one page, combining the slice with router or UI state that the feature should not know about; - is experimental or page-local, so adding it to the shared feature object would widen its public surface for one caller. The rule of thumb: the feature object is the slice's **API**; `extraSelectors` extends that API, and a selectors file composes APIs. ## Pitfalls seen in review - **Accidental overrides.** Naming an extra `selectOrders` to "filter out archived orders" silently changes what every existing `selectOrders` caller receives. Give derived selectors their own names. - **Reading state instead of composing.** The factory receives selectors, not state. Writing `extraSelectors: (state) => ...` and treating the argument as data produces selectors that read the wrong thing. - **Recreating selectors per call.** A factory-shaped extra such as `selectOrderById(id)` returns a new memoized selector each call. How often it is created, and how its cache behaves, is a selector-memoization concern: keep one instance per id where it matters. - **Oversized features.** Twenty page-specific extras turn the feature file into a dumping ground. Keep the slice's own derivations there, and push view models out. ## Key points - `extraSelectors` is a **factory** called with the generated base selectors. - Its result is **spread last**, so a same-named extra **replaces** a generated selector. - Extras that depend on each other are **declared together** inside the factory. - Slice-local derivations go in the feature; **cross-feature joins and page view models** go in their own file.
- An extra selector is named selectOrders and filters out archived orders; what changes for existing callers?Every caller of `ordersFeature.selectOrders` now receives the filtered list. `createFeature` spreads the extras after the generated selectors, and it performs no duplicate-name check, so the extra silently replaces the property selector. Screens that relied on seeing archived orders break without a compile error, because the type is still an order array. Give derived selectors distinct names such as `selectActiveOrders`.
- Why not import customersFeature into the orders feature to build selectOrdersWithCustomerNames there?It couples the orders slice's module to the customers slice, so whoever loads orders also pulls in customers' code, and a reverse join from the customers side would create a circular import. A selector that composes two features' public selectors belongs in a separate file that imports both, which keeps each feature self-contained.
saying these in an interview costs you the question
- extraSelectors takes a plain object of selectors rather than a function.
- The extraSelectors factory receives the feature's current state as its argument.
- An extra selector cannot share a name with a generated selector; NgRx throws at startup.
- Each extra can receive the other extras as arguments, so they compose automatically.
- Every derived selector, including cross-feature joins, belongs in extraSelectors.