NgRx Selectors derive data from the store — filtering, sorting, and computing values without adding redundant data to the store. They are memoised: if inputs have not changed, the selector returns the cached result rather than recomputing. NgRx Effects handle side effects — primarily HTTP requests — by listening to the actions stream, calling services, and dispatching new actions for success and failure. Effects keep reducers pure (no HTTP calls in reducers) and components thin (no HTTP calls in components).
Selectors
import { createSelector, createFeatureSelector } from '@ngrx/store';
// ── Feature selector — root entry point ───────────────────────────────────
const selectPostsState = createFeatureSelector<PostsState>('posts');
// OR use the auto-generated one: postsFeature.selectPostsState
// ── Composed selectors — derived data ─────────────────────────────────────
export const selectAllPosts = createSelector(
postsFeature.selectPosts,
posts => posts
);
export const selectPublishedPosts = createSelector(
postsFeature.selectPosts,
posts => posts.filter(p => p.isPublished)
);
// ── Parameterised selectors (selector factories) ──────────────────────────
export const selectPostById = (id: number) => createSelector(
postsFeature.selectPosts,
posts => posts.find(p => p.id === id)
);
// ── Combining multiple feature selectors ──────────────────────────────────
export const selectDashboardData = createSelector(
postsFeature.selectPosts,
postsFeature.selectTotal,
postsFeature.selectLoading,
(posts, total, loading) => ({ posts, total, loading })
);
// ── Using selectors in components ─────────────────────────────────────────
@Component({ standalone: true, template: `
@if (loading$ | async) { <app-spinner /> }
@for (post of posts$ | async; track post?.id) {
<app-post-card [post]="post!" />
}
` })
export class PostListComponent implements OnInit {
private store = inject(Store);
posts$ = this.store.select(selectPublishedPosts);
loading$ = this.store.select(postsFeature.selectLoading);
ngOnInit() {
this.store.dispatch(PostsActions.loadPosts({ page: 1, size: 10 }));
}
}
Effects
import { createEffect, Actions, ofType } from '@ngrx/effects';
import { inject } from '@angular/core';
import { switchMap, map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
// ── Effects class ─────────────────────────────────────────────────────────
@Injectable()
export class PostsEffects {
private actions$ = inject(Actions);
private api = inject(PostsApiService);
private router = inject(Router);
// ── Load posts effect ─────────────────────────────────────────────────
loadPosts$ = createEffect(() =>
this.actions$.pipe(
ofType(PostsActions.loadPosts), // only handle this action
switchMap(({ page, size }) => // cancel previous on new dispatch
this.api.getPublished(page, size).pipe(
map(result => PostsActions.loadPostsSuccess({
posts: result.items,
total: result.total,
})),
catchError(err =>
of(PostsActions.loadPostsFailure({ error: err.message }))
),
)
)
)
);
// ── Delete post effect ────────────────────────────────────────────────
deletePost$ = createEffect(() =>
this.actions$.pipe(
ofType(PostsActions.deletePost),
switchMap(({ id }) =>
this.api.delete(id).pipe(
map(() => PostsActions.deletePostSuccess({ id })),
catchError(err =>
of(PostsActions.deletePostFailure({ error: err.message }))
)
)
)
)
);
// ── Navigate after create ─────────────────────────────────────────────
navigateAfterCreate$ = createEffect(() =>
this.actions$.pipe(
ofType(PostsActions.createPostSuccess),
tap(({ post }) => this.router.navigate(['/posts', post.slug]))
),
{ dispatch: false } // this effect does not dispatch another action
);
}
// ── Register effects ───────────────────────────────────────────────────────
// provideEffects(PostsEffects) in app.config.ts or route providers
this.api.*() call), not at the outer level. If the catchError is outside the switchMap, an error terminates the entire actions stream — no more loadPosts actions will ever be handled. By placing catchError inside the switchMap callback, errors only terminate the inner HTTP Observable and return a failure action — the outer stream continues listening for future loadPosts dispatches.{ dispatch: false } on effects that only perform side effects (navigation, toasts) without dispatching another action.Common Mistakes
Mistake 1 — catchError outside switchMap (terminates the effect permanently after first error)
❌ Wrong — actions$.pipe(ofType(...), switchMap(...), catchError(...)); first HTTP error kills the effect forever.
✅ Correct — catchError must be inside switchMap(action => api.call().pipe(catchError(...))).
Mistake 2 — Forgetting dispatch: false for navigation/toast effects (TypeError: actions must be objects)
❌ Wrong — navigation effect returns void; NgRx tries to dispatch void as an action and throws.
✅ Correct — add { dispatch: false } to effects that do not produce a new action to dispatch.