The createFeature API in NgRx
The createFeature function made its debut in NgRx v12.1.
It trims down repetitive code in selector files by automatically creating a feature selector and child selectors for every property of a feature state. The design draws inspiration from the ngrx-child-selectors library.
Anatomy of an NgRx Feature
Global state handling with @ngrx/store rests on three pillars: actions, reducers, and selectors. For each feature state, we typically need a reducer to manage state changes in response to dispatched actions, and selectors to pull out specific slices of state. Additionally, a feature name is required to register the feature reducer with the store. In essence, an NgRx feature is a bundle containing the feature name, reducer, and selectors for a given state slice. Here’s the conventional approach to setting this up.
For the reducer, the createReducer helper from @ngrx/store is used:
// books.reducer.ts
import { createReducer } from "@ngrx/store";
import * as BookListPageActions from "./book-list-page.actions";
import * as BooksApiActions from "./books-api.actions";
export const featureName = "books";
export interface State {
books: Book[];
loading: boolean;
}
const initialState: State = {
books: [],
loading: false,
};
export const reducer = createReducer(
initialState,
on(BookListPageActions.enter, (state) => ({
...state,
loading: true,
})),
on(BooksApiActions.loadBooksSuccess, (state, { books }) => ({
...state,
books,
loading: false,
}))
);
To register that reducer, the StoreModule.forFeature method comes into play:
// books.module.ts
import { StoreModule } from "@ngrx/store";
import * as fromBooks from "./books.reducer";
@NgModule({
imports: [
StoreModule.forFeature(fromBooks.featureName, fromBooks.reducer),
],
})
export class BooksModule {}
For pulling data from the store, we'd craft a feature selector, child selectors, plus a view model selector:
// books.selectors.ts
import { createFeatureSelector, createSelector } from "@ngrx/store";
import * as fromBooks from "./books.reducer";
// feature selector
export const selectBooksState = createFeatureSelector<fromBooks.State>(
fromBooks.featureKey
);
// child selectors
export const selectBooks = createSelector(
selectBooksState,
(state) => state.books
);
export const selectLoading = createSelector(
selectBooksState,
(state) => state.loading
);
// view model selector
export const selectBookListPageViewModel = createSelector(
selectBooks,
selectLoading,
(books, loading) => ({ books, loading })
);
Adopting the Feature Creator
Now, let's explore how the createFeature function achieves the same outcome. The first step is to modify the reducer file:
// `createFeature` is imported from `@ngrx/store`
import { createFeature, createReducer } from "@ngrx/store";
import * as BookListPageActions from "./book-list-page.actions";
import * as BooksApiActions from "./books-api.actions";
interface State {
books: Book[];
loading: boolean;
}
const initialState: State = {
books: [],
loading: false,
};
// feature name and reducer are now passed to `createFeature`
export const booksFeature = createFeature({
name: "books",
reducer: createReducer(
initialState,
on(BookListPageActions.enter, (state) => ({
...state,
loading: true,
})),
on(BooksApiActions.loadBooksSuccess, (state, { books }) => ({
...state,
books,
loading: false,
}))
),
});
With this in place, you can register the feature reducer by passing the whole feature object straight into StoreModule.forFeature:
// books.module.ts
import { StoreModule } from "@ngrx/store";
import { booksFeature } from "./books.reducer";
@NgModule({
imports: [StoreModule.forFeature(booksFeature)],
})
export class BooksModule {}
Finally, here's the updated selector file:
// books.selectors.ts
import { createSelector } from "@ngrx/store";
import { booksFeature } from "./books.reducer";
export const selectBookListPageViewModel = createSelector(
booksFeature.selectBooks,
booksFeature.selectLoading,
(books, loading) => ({ books, loading })
);
The selectors that were previously written by hand are now unnecessary, as createFeature takes care of generating them. Every generated selector carries the "select" prefix, while the feature selector itself ends with the "State" suffix.
In this instance, the feature selector is named selectBooksState, with "books" serving as the feature name. The child selectors, selectBooks and selectLoading, take their names from the properties in the books feature state.
Wrapping Up
Feature creators cut down on boilerplate in selector files, thanks to template literal types from TypeScript v4.1. This approach can substantially streamline your code, particularly when dealing with large feature states.
Further Reading
Reviewers
Thanks to Tim for the valuable feedback on this piece!
The "NgRx Feature Creator" guide is now part of the official NgRx docs. Find it here.
