In NgRx the entire application state lives in one big store. Two questions follow immediately: how do you read exactly the slice you need, and how do you connect the store to the outside world (the server)? This lesson answers both with two tools: a selector is a window onto a slice of state, and an effect is a bridge between an action and an HTTP request. Let us start with reading.
A selector is a function that pulls one piece out of the store. It has a superpower called memoization: it recomputes only when its own dependencies change - the rest of the time it simply hands back the remembered result.
1import { createFeatureSelector, createSelector } from '@ngrx/store';
2
3export const selectSamuraiState = createFeatureSelector<SamuraiState>('samurai');
4
5export const selectAllSamurai = createSelector(
6 selectSamuraiState,
7 state => state.samurai
8);
9
10export const selectSamuraiCount = createSelector(
11 selectAllSamurai,
12 samurai => samurai.length
13);Read it from the top.
createFeatureSelector<SamuraiState>('samurai') selects an entire branch of the state - the feature registered under the name samurai. That is all it does: it does not register the reducer in the module, it does not define a loading action, and it is not an effect. Every createSelector call below it follows the same shape: input selectors first, the projector function last. selectAllSamurai reads the list out of the feature, and selectSamuraiCount composes on top of it and returns the length; the very same pattern gives you selectLoading from state.loading.That composition is where memoization pays off. Much like
computed with signals, selectSamuraiCount recomputes only when the list changes, not on every other store update - so the view never re-renders for nothing. And memoization really is the whole benefit: a selector does not save anything to localStorage, does not sort your results, and performs no runtime type validation.When you need a selector with a parameter (by
id, for example), you return a selector from a function:1export const selectSamuraiById = (id: number) => createSelector(
2 selectAllSamurai,
3 samurai => samurai.find(s => s.id === id)
4);
5
6export const selectSamuraiByClan = (clan: string) => createSelector(
7 selectAllSamurai,
8 samurai => samurai.filter(s => s.clan === clan)
9);The pattern is the same one you met with custom validators: the outer function takes the parameter (
id, clan) and returns a ready selector. Notice what stays fixed - the input selector is always selectAllSamurai, and only the projection function changes. From that single source you build any view you like, one samurai or a whole clan, with no duplicated logic. In a component you read it with store.select(selectSamuraiByClan('Iga')).A selector only reads. So how does the data get into the store in the first place? This is where the effect comes in: it listens for an action (say, "load the samurai"), does something with the outside world - an HTTP request - and returns a new action carrying the result. Effects are meant for exactly that kind of side effect; they are not for UI animations, not for styling components, and not for form validation. Each one lives in an
@Injectable() class where the stream of every dispatched action arrives as private actions$ = inject(Actions), which is why the chain always starts with this.actions$.pipe(...).1loadSamurai$ = createEffect(() =>
2 this.actions$.pipe(
3 ofType(loadSamurai), // listen for this action
4 switchMap(() =>
5 this.http.get<Samurai[]>('/api/samurai').pipe(
6 map(samurai => loadSamuraiSuccess({ samurai })), // success -> action
7 catchError(error => of(loadSamuraiFailure({ error: error.message })))
8 )
9 )
10 )
11);Walk across the bridge in order:
ofType(loadSamurai) listens for the action and lets only that one through, switchMap calls the API, map turns the response into the success action loadSamuraiSuccess, and catchError turns a failure into the error action loadSamuraiFailure. That sequence - listen for the action, call the API, success action, error action - is the skeleton of every loading effect you will write. It also shows the foundation of NgRx: an effect never changes the store directly, it only emits an action that the reducer translates into new state. An error is an action too, not a thrown exception, which is why catchError returns of(...Failure).The choice of operator matters just as much - compare it with the version used for adding:
1addSamurai$ = createEffect(() =>
2 this.actions$.pipe(
3 ofType(addSamurai),
4 exhaustMap(({ samurai }) => // ignore new ones while one is running
5 this.http.post<Samurai>('/api/samurai', samurai).pipe(
6 map(saved => addSamuraiSuccess({ samurai: saved })),
7 catchError(error => of(addSamuraiFailure({ error: error.message })))
8 )
9 )
10 )
11);The difference is
switchMap versus exhaustMap. When loading, switchMap cancels the old request in favor of the newest one - only the freshest result counts. When saving, exhaustMap does the opposite: it ignores further clicks while the previous request is still in flight, so a double click on "Save" can never create two samurai. Remember that pair: switchMap for fetching, exhaustMap for saving.Keep two roles from this lesson. A selector is the window you read a slice of state through, and it recomputes only when its dependencies change. An effect is the bridge that turns an action into work in the outside world and back into a new action. The store stays pure, and the whole conversation with the server flows through effects.