In NgRx 22, how do you define an orders slice with createFeature and register it only when a lazy admin route loads?
answer
- name plus reducer, one object
- selectors named after state keys
- route providers, not component providers
- provideState(feature) on the route
- update-reducers action seeds the slice
basics
~20 screateFeature({ name: 'orders', reducer }) bundles the reducer with a feature selector and one selector per state property. Putting provideState(ordersFeature) in the admin route's providers adds the slice to the single store when that route first activates.
solid answer
~40 s`createFeature({ name: 'orders', reducer: createReducer(initialState, ...) })` returns the `name`, the `reducer`, a feature selector `selectOrdersState`, and one selector per top-level state property, such as `selectOrders`, `selectSelectedId` and `selectLoading`, generated from the keys of the initial state. The app bootstraps `provideStore()` once; the admin route lists `provideState(ordersFeature)` in its `providers`. When that route activates, NgRx adds the reducer to the one global store under the key `'orders'` and dispatches `@ngrx/store/update-reducers`, so the slice starts at its initial state. `provideState` works only in environment injectors (application or route providers), never in a component's `providers`. Because the generated selectors read `state.orders`, the registration key must equal the feature `name`.
code
ts · 24 lines// orders.feature.ts
import { createFeature, createReducer, on } from '@ngrx/store';
export interface OrdersState {
orders: Order[];
selectedId: string | null; // not selectedId?: string
loading: boolean;
}
const initialState: OrdersState = { orders: [], selectedId: null, loading: false };
export const ordersFeature = createFeature({
name: 'orders',
reducer: createReducer(
initialState,
on(OrdersPageActions.opened, (state) => ({ ...state, loading: true })),
on(OrdersApiActions.loadSucceeded, (state, { orders }) => ({
...state,
orders,
loading: false,
}))
),
});
// ordersFeature.selectOrdersState, .selectOrders, .selectSelectedId, .selectLoadinggo deeper
Recall that createFeature takes a name and a reducer, that its selectors are named select plus the property name, and that provideState registers the feature.
Explain how the property selectors come from the initial-state keys, why optional properties are rejected, and what the update-reducers action does when a lazy route registers the slice.
Diagnose undefined selector results from a key mismatch or a read before registration, and keep each slice's code inside the lazily loaded chunk that owns it.
Decide which slices deserve eager registration because many areas read them and which stay lazy, weighing initial bundle size against cross-area coupling.
## What an NgRx feature is The NgRx **global Store** is one state tree. Each top-level key of that tree is a **feature** (or slice), owned by one reducer. A feature always needs three things that must agree with each other: - a **name**, the key the slice lives under in the root state; - a **reducer**, built with `createReducer` and `on()`; - **selectors** that read the slice back out, starting from `state[name]`. `createFeature` bundles the three so they cannot drift apart. You pass `{ name, reducer }`; you get back an object with the name, the reducer, and generated selectors. ## What createFeature generates For an admin `orders` slice whose initial state is `{ orders: [], selectedId: null, loading: false }`, the returned object contains: | Member | What it is | |---|---| | `name` | `'orders'`, the registration key | | `reducer` | the reducer you passed in | | `selectOrdersState` | the **feature selector**: `select` + capitalised name + `State` | | `selectOrders` | reads `state.orders.orders` | | `selectSelectedId` | reads `state.orders.selectedId` | | `selectLoading` | reads `state.orders.loading` | Two details come from how the property selectors are built: 1. NgRx calls your reducer once with `undefined` state to obtain the **initial state**, then creates one memoized selector per key of that object. A property missing from the initial state gets no selector. 2. For that reason the type of `createFeature` **rejects optional properties** in the feature state. Write `selectedId: string | null` with `selectedId: null` in the initial state, not `selectedId?: string`. Derived selectors, such as "orders awaiting shipment", can be added through the `extraSelectors` option; general selector composition is its own topic. ## Registering the slice with a lazy route In a standalone NgRx 22 application, registration has two halves: 1. **Once, at the root:** `provideStore()` in the providers passed to `bootstrapApplication` (or in `app.config.ts`). It creates the store, even with no root reducers. Providing the root store a second time throws "The root Store has been provided more than once". 2. **Per feature, where it is needed:** `provideState(ordersFeature)` in the `providers` array of the route that owns the admin area. When the router activates that route for the first time, its environment injector runs the feature providers. NgRx adds the orders reducer to the reducer map and dispatches an internal action, `@ngrx/store/update-reducers`, listing the new feature key. The new reducer receives `undefined` for its slice and returns its initial state, so `state.orders` exists from that moment. There is no gap in which the slice is registered but empty. `provideState` also accepts `(name, reducer, config?)` for a hand-built reducer, and the older NgModule spelling of the same registration is `StoreModule.forFeature(ordersFeature)`. ## Mistakes worth recognising - **Component-level providers.** `provideState` returns environment providers. It belongs in application or route providers; the JSDoc states that it cannot be used at the component level. - **A second `provideStore`** in the lazy route, in the belief that each area needs its own store. There is only one store; features plug into it. - **Mismatched keys.** `provideState('adminOrders', ordersFeature.reducer)` stores the slice under `adminOrders`, but every generated selector reads `state.orders`. The selectors return `undefined`, and in development `createFeatureSelector` logs a warning that the feature name does not exist in the state. - **Reading before registration.** A component outside the admin area can select the orders slice, but until the admin route has activated, the key is absent and the selectors yield `undefined`, with the same development warning. ## Why teams use createFeature - It removes a hand-written feature selector and one selector per property, all of which had to repeat the feature key. - The **name is defined once**, so registration and selection cannot disagree when you pass the whole feature object to `provideState`. - The generated selectors are ordinary memoized selectors, usable directly with `store.select` or `selectSignal`, or as inputs to your own. - It keeps the slice's definition in one file that the lazy route imports, so the orders code stays inside the lazily loaded chunk. ## Key points - `createFeature` = **name + reducer + generated selectors**, one per initial-state key. - **No optional properties** in feature state; use `| null`. - `provideStore()` **once**; `provideState(feature)` in **route or application providers**. - Registration dispatches **`@ngrx/store/update-reducers`**, and the slice starts at its initial state.
- What goes wrong if the admin route registers provideState('adminOrders', ordersFeature.reducer)?The slice is stored under `adminOrders`, but `createFeature` built its feature selector from the name `'orders'`, so `selectOrdersState` and every property selector read a key that does not exist. They return `undefined`, and in development `createFeatureSelector` logs that the feature name is missing from the state. Passing the whole feature object to `provideState` keeps the key and the selectors in agreement.
- Why does createFeature refuse a state interface with an optional property?Its property selectors are generated at runtime from the keys of the initial state, obtained by calling the reducer with `undefined`. An optional property usually has no key in that object, so it would get no selector even though the type suggested one. The type of `createFeature` therefore rejects optional properties; declare the field as `T | null` and give it a value in the initial state.
saying these in an interview costs you the question
- provideState can go in a component's providers array to scope a slice to that component.
- Each lazily loaded area needs its own provideStore call.
- createFeature generates selectors for optional properties, even ones missing from the initial state.
- The feature name is only a label; the generated selectors work under any registration key.
- A lazily registered slice stays undefined until its first action is dispatched.