NgRx best practices for actions, reducers, selectors, and effects. Auto-invoked when working with NgRx store files.
Reference: https://angular.dev/llms.txt | https://ngrx.io
createAction with props<T>() — never string literals[Feature] Event Description (e.g., [Products] Load Success)export const loadProducts = createAction('[Products] Load');
export const loadProductsSuccess = createAction('[Products] Load Success', props<{ items: Product[] }>());
export const loadProductsFailure = createAction('[Products] Load Failure', props<{ error: string }>());
createReducer with typed state interfaceon() handlers must be pure — no side effects, no mutation{ ...state, changed: value }FEATURE_KEY constant alongside reducerexport const PRODUCTS_FEATURE_KEY = 'products';
export interface ProductsState {
items: Product[];
loading: boolean;
error: string | null;
}
const initialState: ProductsState = { items: [], loading: false, error: null };
export const productsReducer = createReducer(
initialState,
on(ProductActions.loadProducts, state => ({ ...state, loading: true, error: null })),
on(ProductActions.loadProductsSuccess, (state, { items }) => ({ ...state, loading: false, items })),
on(ProductActions.loadProductsFailure, (state, { error }) => ({ ...state, loading: false, error })),
);
createSelector — never access store state directly in componentscreateFeatureSelector as the root for a feature sliceexport const selectProductsState = createFeatureSelector<ProductsState>(PRODUCTS_FEATURE_KEY);
export const selectProductItems = createSelector(selectProductsState, s => s.items);
export const selectProductsLoading = createSelector(selectProductsState, s => s.loading);
// Derived selectors — compose from primitives
export const selectActiveProducts = createSelector(
selectProductItems,
items => items.filter(p => p.active)
);
Test selectors with .projector() — no store needed:
expect(selectActiveProducts.projector(mockItems)).toEqual(expected);
{ functional: true }catchError returning a failure actionswitchMap for cancellable ops (search), concatMap for ordered, mergeMap for parallel, exhaustMap for non-interruptible (login)export const loadProductsEffect = createEffect(
(actions$ = inject(Actions), service = inject(ProductsService)) =>
actions$.pipe(
ofType(ProductActions.loadProducts),
switchMap(() =>
service.getAll().pipe(
map(items => ProductActions.loadProductsSuccess({ items })),
catchError(err => of(ProductActions.loadProductsFailure({ error: err.message }))),
),
),
),
{ functional: true },
);
Use EntityAdapter for collections instead of arrays + manual manipulation:
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
export interface ProductsState extends EntityState<Product> {
loading: boolean;
error: string | null;
}
export const adapter: EntityAdapter<Product> = createEntityAdapter<Product>();
export const initialState: ProductsState = adapter.getInitialState({ loading: false, error: null });
// Reducer
on(ProductActions.loadProductsSuccess, (state, { items }) =>
adapter.setAll(items, { ...state, loading: false })
),
// Selectors (auto-generated)
const { selectAll, selectEntities, selectIds, selectTotal } = adapter.getSelectors();
export const selectAllProducts = createSelector(selectProductsState, selectAll);
For standalone apps, register in app.config.ts or feature providers:
// Root (app.config.ts)
provideStore(),
provideEffects(),
provideStoreDevtools({ maxAge: 25 }),
// Feature (lazy route providers)
provideState(PRODUCTS_FEATURE_KEY, productsReducer),
provideEffects([loadProductsEffect]),
export class ProductsComponent {
private readonly store = inject(Store);
readonly products = this.store.selectSignal(selectAllProducts);
readonly loading = this.store.selectSignal(selectProductsLoading);
ngOnInit() {
this.store.dispatch(ProductActions.loadProducts());
}
}
Prefer selectSignal() over select() + async pipe for signal-based components.