Angular Ngrx
NgRx state management with store, effects, selectors, and the component store
You are an expert in NgRx for building scalable, predictable state management in Angular applications.
## Key Points
1. **Use `createActionGroup` for concise action definitions.** It eliminates repetitive `createAction` calls and enforces consistent naming.
2. **Use functional effects.** The `{ functional: true }` syntax is cleaner, tree-shakable, and aligns with Angular's move toward functions.
3. **Use `selectSignal` in components.** It integrates NgRx state with Angular signals, removing the need for `async` pipe and reducing subscription management.
4. **Use `createFeature` to auto-generate selectors.** It creates selectors for every top-level state property, saving boilerplate.
5. **Use ComponentStore or SignalStore for local state.** Not everything belongs in the global store. Pagination, form state, and UI toggles are better handled locally.
6. **Keep reducers pure and effects lean.** Reducers should only compute new state. Effects should orchestrate side effects and dispatch result actions.
- **Over-using the global store.** Putting everything in NgRx adds overhead. Use the global store for shared, cross-feature state. Use ComponentStore or signals for local state.
- **Dispatching actions in constructors without guards.** This can cause redundant API calls when components re-render. Check if data is already loaded before dispatching.
- **Fat actions vs. fat reducers.** Prefer actions that describe *what happened* (events) rather than *what to do* (commands). Let the reducer decide how state changes.
- **Not using `selectSignal` or `async` pipe.** Manually subscribing to `store.select()` and assigning to properties creates memory leak risk and requires cleanup.
- **Forgetting to register effects.** Effects must be registered via `provideEffects()` at the correct level (root or feature). Unregistered effects simply never run.skilldb get angular-skills/Angular NgrxFull skill: 447 linesNgRx State Management — Angular
You are an expert in NgRx for building scalable, predictable state management in Angular applications.
Core Philosophy
NgRx implements the Redux pattern for Angular with a clear separation: actions describe what happened, reducers compute new state, selectors derive data for the UI, and effects handle side effects. This separation is not bureaucratic overhead — it creates a unidirectional data flow where every state change is traceable, reproducible, and debuggable through Redux DevTools. When a bug appears, you can replay the action log to find exactly which event caused the wrong state.
The most important architectural decision with NgRx is knowing what belongs in the global store and what does not. Global store state should be shared across multiple features, needed by routes that are not simultaneously rendered, or required for undo/redo capability. Local UI state (pagination, form inputs, toggle states) belongs in ComponentStore or signals. Putting everything in the global store creates excessive boilerplate and makes simple UI interactions require action/reducer/selector ceremonies that slow development without adding value.
Modern NgRx (v16+) dramatically reduces boilerplate through createFeature (auto-generating selectors), createActionGroup (concise action definitions), functional effects, and selectSignal (bridging NgRx with Angular signals). The signalStore API represents the future direction: a signal-native store that combines the benefits of NgRx's patterns with Angular's signal-based reactivity. New features should strongly consider signalStore before reaching for the traditional action/reducer/effect pattern.
Anti-Patterns
-
Over-Using the Global Store — putting every piece of state into NgRx, including form values, UI toggles, and component-local pagination. This creates massive boilerplate overhead for state that only one component uses. Use ComponentStore or signals for local state.
-
Fat Actions (Commands Instead of Events) — naming actions like
SetUsersorUpdateLoadingStateinstead of event-oriented names likeUsersPageOpenedorUsersLoadedSuccess. Command-style actions couple the action to a specific state change, defeating the purpose of indirection. -
Dispatching in Constructors Without Guards — calling
store.dispatch(loadProducts())inngOnInitwithout checking if products are already loaded, causing redundant API calls every time the component initializes. -
Manual Subscriptions to Store Selects — using
store.select(...).subscribe(val => this.data = val)instead ofselectSignalor theasyncpipe, creating subscription management burdens and memory leak risks. -
Unregistered Effects — defining an effect function but forgetting to include it in
provideEffects()at the correct level. The effect compiles without error but simply never executes, causing silent data-fetching failures.
Overview
NgRx is a reactive state management library for Angular built on RxJS. It implements the Redux pattern — a single immutable store, actions describing events, reducers computing new state, and selectors deriving data. NgRx also provides Effects for handling side effects and ComponentStore for local component state. In modern NgRx (v16+), the createFeature and signalStore APIs simplify boilerplate significantly.
Core Concepts
Actions
Actions are unique events dispatched from components, services, or effects:
// products.actions.ts
import { createActionGroup, emptyProps, props } from '@ngrx/store';
export const ProductActions = createActionGroup({
source: 'Products',
events: {
'Load Products': emptyProps(),
'Load Products Success': props<{ products: Product[] }>(),
'Load Products Failure': props<{ error: string }>(),
'Select Product': props<{ productId: string }>(),
'Delete Product': props<{ productId: string }>(),
'Delete Product Success': props<{ productId: string }>(),
},
});
Reducers with createFeature
// products.reducer.ts
import { createFeature, createReducer, on } from '@ngrx/store';
import { ProductActions } from './products.actions';
export interface ProductState {
products: Product[];
selectedProductId: string | null;
loading: boolean;
error: string | null;
}
const initialState: ProductState = {
products: [],
selectedProductId: null,
loading: false,
error: null,
};
export const productsFeature = createFeature({
name: 'products',
reducer: createReducer(
initialState,
on(ProductActions.loadProducts, (state) => ({
...state,
loading: true,
error: null,
})),
on(ProductActions.loadProductsSuccess, (state, { products }) => ({
...state,
products,
loading: false,
})),
on(ProductActions.loadProductsFailure, (state, { error }) => ({
...state,
loading: false,
error,
})),
on(ProductActions.selectProduct, (state, { productId }) => ({
...state,
selectedProductId: productId,
})),
on(ProductActions.deleteProductSuccess, (state, { productId }) => ({
...state,
products: state.products.filter(p => p.id !== productId),
})),
),
});
// createFeature auto-generates selectors:
// productsFeature.selectProducts
// productsFeature.selectLoading
// productsFeature.selectError
// productsFeature.selectSelectedProductId
Selectors
Compose selectors for derived data:
// products.selectors.ts
import { createSelector } from '@ngrx/store';
import { productsFeature } from './products.reducer';
export const selectSelectedProduct = createSelector(
productsFeature.selectProducts,
productsFeature.selectSelectedProductId,
(products, selectedId) =>
selectedId ? products.find(p => p.id === selectedId) ?? null : null,
);
export const selectProductCount = createSelector(
productsFeature.selectProducts,
(products) => products.length,
);
export const selectAvailableProducts = createSelector(
productsFeature.selectProducts,
(products) => products.filter(p => p.inStock),
);
Effects
Effects handle side effects like HTTP requests:
// products.effects.ts
import { inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { ProductService } from '../services/product.service';
import { ProductActions } from './products.actions';
import { catchError, exhaustMap, map, of, mergeMap } from 'rxjs';
export const loadProducts = createEffect(
(actions$ = inject(Actions), productService = inject(ProductService)) =>
actions$.pipe(
ofType(ProductActions.loadProducts),
exhaustMap(() =>
productService.getAll().pipe(
map(products => ProductActions.loadProductsSuccess({ products })),
catchError(error =>
of(ProductActions.loadProductsFailure({ error: error.message }))
),
)
),
),
{ functional: true }
);
export const deleteProduct = createEffect(
(actions$ = inject(Actions), productService = inject(ProductService)) =>
actions$.pipe(
ofType(ProductActions.deleteProduct),
mergeMap(({ productId }) =>
productService.delete(productId).pipe(
map(() => ProductActions.deleteProductSuccess({ productId })),
catchError(error =>
of(ProductActions.loadProductsFailure({ error: error.message }))
),
)
),
),
{ functional: true }
);
Implementation Patterns
Store Registration
// app.config.ts
import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { productsFeature } from './products/state/products.reducer';
import * as productEffects from './products/state/products.effects';
export const appConfig: ApplicationConfig = {
providers: [
provideStore(),
provideEffects(productEffects),
provideStoreDevtools({ maxAge: 25, logOnly: !isDevMode() }),
],
};
// Lazy-loaded feature registration
// products/product.routes.ts
import { provideState } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
export const PRODUCT_ROUTES: Routes = [
{
path: '',
providers: [
provideState(productsFeature),
provideEffects(productEffects),
],
loadComponent: () =>
import('./product-shell.component').then(m => m.ProductShellComponent),
},
];
Component Usage with selectSignal
@Component({
standalone: true,
imports: [RouterLink],
template: `
@if (loading()) {
<app-spinner />
} @else if (error()) {
<app-error [message]="error()!" />
} @else {
<ul>
@for (product of products(); track product.id) {
<li>
<a [routerLink]="[product.id]" (click)="select(product.id)">
{{ product.name }} - {{ product.price | currency }}
</a>
</li>
}
</ul>
}
`,
})
export class ProductListComponent implements OnInit {
private store = inject(Store);
products = this.store.selectSignal(productsFeature.selectProducts);
loading = this.store.selectSignal(productsFeature.selectLoading);
error = this.store.selectSignal(productsFeature.selectError);
ngOnInit(): void {
this.store.dispatch(ProductActions.loadProducts());
}
select(productId: string): void {
this.store.dispatch(ProductActions.selectProduct({ productId }));
}
}
ComponentStore for Local State
For component-scoped state that does not need to be global:
import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { tapResponse } from '@ngrx/operators';
import { switchMap, tap } from 'rxjs';
interface PaginationState {
items: Item[];
page: number;
pageSize: number;
total: number;
loading: boolean;
}
@Injectable()
export class PaginationStore extends ComponentStore<PaginationState> {
private itemService = inject(ItemService);
constructor() {
super({ items: [], page: 1, pageSize: 20, total: 0, loading: false });
}
// Selectors
readonly items = this.selectSignal(s => s.items);
readonly page = this.selectSignal(s => s.page);
readonly totalPages = this.selectSignal(s => Math.ceil(s.total / s.pageSize));
readonly loading = this.selectSignal(s => s.loading);
// Updaters
readonly setPage = this.updater((state, page: number) => ({
...state,
page,
}));
// Effects
readonly loadItems = this.effect<{ page: number; pageSize: number }>(
(params$) =>
params$.pipe(
tap(() => this.patchState({ loading: true })),
switchMap(({ page, pageSize }) =>
this.itemService.getItems(page, pageSize).pipe(
tapResponse(
(response) =>
this.patchState({
items: response.items,
total: response.total,
loading: false,
}),
(error: Error) => {
console.error(error);
this.patchState({ loading: false });
},
),
)
),
)
);
}
@Component({
standalone: true,
providers: [PaginationStore],
template: `
@for (item of store.items(); track item.id) {
<div>{{ item.name }}</div>
}
<app-paginator
[currentPage]="store.page()"
[totalPages]="store.totalPages()"
(pageChange)="onPageChange($event)"
/>
`,
})
export class ItemListComponent implements OnInit {
protected store = inject(PaginationStore);
ngOnInit(): void {
this.store.loadItems({ page: 1, pageSize: 20 });
}
onPageChange(page: number): void {
this.store.setPage(page);
this.store.loadItems({ page, pageSize: 20 });
}
}
NgRx SignalStore (v17+)
The newest NgRx API built entirely on signals:
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { computed, inject } from '@angular/core';
import { pipe, switchMap, tap } from 'rxjs';
import { tapResponse } from '@ngrx/operators';
type TodoState = {
todos: Todo[];
loading: boolean;
filter: 'all' | 'active' | 'completed';
};
export const TodoStore = signalStore(
{ providedIn: 'root' },
withState<TodoState>({ todos: [], loading: false, filter: 'all' }),
withComputed((store) => ({
filteredTodos: computed(() => {
const filter = store.filter();
const todos = store.todos();
switch (filter) {
case 'active': return todos.filter(t => !t.completed);
case 'completed': return todos.filter(t => t.completed);
default: return todos;
}
}),
completedCount: computed(() => store.todos().filter(t => t.completed).length),
})),
withMethods((store, todoService = inject(TodoService)) => ({
setFilter(filter: 'all' | 'active' | 'completed') {
patchState(store, { filter });
},
loadTodos: rxMethod<void>(
pipe(
tap(() => patchState(store, { loading: true })),
switchMap(() =>
todoService.getAll().pipe(
tapResponse(
(todos) => patchState(store, { todos, loading: false }),
(err) => {
console.error(err);
patchState(store, { loading: false });
},
)
)
),
)
),
toggleTodo(id: number) {
patchState(store, {
todos: store.todos().map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
),
});
},
})),
);
Best Practices
-
Use
createActionGroupfor concise action definitions. It eliminates repetitivecreateActioncalls and enforces consistent naming. -
Use functional effects. The
{ functional: true }syntax is cleaner, tree-shakable, and aligns with Angular's move toward functions. -
Use
selectSignalin components. It integrates NgRx state with Angular signals, removing the need forasyncpipe and reducing subscription management. -
Use
createFeatureto auto-generate selectors. It creates selectors for every top-level state property, saving boilerplate. -
Use ComponentStore or SignalStore for local state. Not everything belongs in the global store. Pagination, form state, and UI toggles are better handled locally.
-
Keep reducers pure and effects lean. Reducers should only compute new state. Effects should orchestrate side effects and dispatch result actions.
Common Pitfalls
-
Over-using the global store. Putting everything in NgRx adds overhead. Use the global store for shared, cross-feature state. Use ComponentStore or signals for local state.
-
Dispatching actions in constructors without guards. This can cause redundant API calls when components re-render. Check if data is already loaded before dispatching.
-
Fat actions vs. fat reducers. Prefer actions that describe what happened (events) rather than what to do (commands). Let the reducer decide how state changes.
-
Not using
selectSignalorasyncpipe. Manually subscribing tostore.select()and assigning to properties creates memory leak risk and requires cleanup. -
Forgetting to register effects. Effects must be registered via
provideEffects()at the correct level (root or feature). Unregistered effects simply never run.
Install this skill directly: skilldb add angular-skills
Related Skills
Angular Testing
Testing Angular applications with Jest for unit tests and Cypress for end-to-end tests
Angular Dependency Injection
Angular dependency injection system including providers, injection tokens, and hierarchical injectors
Angular Forms
Reactive forms, form validation, dynamic forms, and typed form controls in Angular
Angular Routing
Angular Router configuration including lazy loading, guards, resolvers, and nested routes
Angular Rxjs Patterns
RxJS reactive patterns for data fetching, state management, and event handling in Angular
Angular Signals
Angular Signals for fine-grained reactivity and efficient change detection