We use cookies to enhance your experience on the site
CodeWorlds

Selectors and Effects - windows and bridges in NgRx

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.

Selector - a window onto a slice of state

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'))
.

Effect - a bridge between an action and the server

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.

Go to CodeWorlds