Kurs Angular · Moduł 10: Zarządzanie stanem

Selektory i efekty - okna i mosty w NgRx

3 min czytania ·
W tej lekcji2

W NgRx cały stan aplikacji leży w jednym wielkim magazynie (store). Rodzą się dwa pytania: jak czytać z niego dokładnie to, czego chcę, i jak łączyć go ze światem zewnętrznym (serwerem)? Odpowiedzią są dwa narzędzia z tej lekcji: selektor to okno na wycinek stanu, a effect to most między akcją a HTTP. Zacznijmy od czytania.

Selektor - okno na wycinek stanu

Selektor to funkcja, która wyciąga ze store'a jeden kawałek. Ma supermoc: przelicza się tylko, gdy jego dane naprawdę się zmienią - reszta czasu zwraca zapamiętany wynik.

import { createFeatureSelector, createSelector } from '@ngrx/store';

export const selectSamuraiState = createFeatureSelector<SamuraiState>('samurai');

export const selectAllSamurai = createSelector(
  selectSamuraiState,
  state => state.samurai
);

export const selectSamuraiCount = createSelector(
  selectAllSamurai,
  samurai => samurai.length
);

Zauważ, jak selektory się składają: selectAllSamurai czyta listę ze stanu, a selectSamuraiCount buduje się na nim i liczy długość. To jak computed przy sygnałach - selectSamuraiCount przeliczy się dopiero, gdy zmieni się lista, nie przy każdej innej zmianie store'a. Dzięki temu widok nie odświeża się bez potrzeby.

Gdy potrzebujesz selektora z parametrem (np. po id), zwracasz selektor z funkcji:

export const selectSamuraiById = (id: number) => createSelector(
  selectAllSamurai,
  samurai => samurai.find(s => s.id === id)
);

export const selectSamuraiByClan = (clan: string) => createSelector(
  selectAllSamurai,
  samurai => samurai.filter(s => s.clan === clan)
);

Wzorzec jest ten sam co przy własnych walidatorach: funkcja zewnętrzna przyjmuje parametr (id, clan) i zwraca gotowy selektor. Dzięki temu z jednego selectAllSamurai budujesz dowolne widoki - jednego samuraja, cały klan - bez duplikowania logiki. W komponencie czytasz to przez store.select(selectSamuraiByClan('Iga')).

Effect - most między akcją a serwerem

Selektor tylko czyta. Ale skąd dane trafiają do store'a? Tu wchodzi effect: nasłuchuje akcji (np. "wczytaj samurajów"), robi coś ze światem (żądanie HTTP) i zwraca nową akcję z wynikiem.

loadSamurai$ = createEffect(() =>
  this.actions$.pipe(
    ofType(loadSamurai),                 // nasluchuj tej akcji
    switchMap(() =>
      this.http.get<Samurai[]>('/api/samurai').pipe(
        map(samurai => loadSamuraiSuccess({ samurai })),   // sukces -> akcja
        catchError(error => of(loadSamuraiFailure({ error: error.message })))
      )
    )
  )
);

Prześledź most: ofType(loadSamurai) przepuszcza tylko interesującą akcję, switchMap odpala żądanie, a wynik zamieniasz na kolejną akcję - loadSamuraiSuccess albo loadSamuraiFailure. To fundament NgRx: effect nigdy nie zmienia store'a wprost, tylko wysyła akcję, którą reduktor przełoży na nowy stan. Błąd też jest akcją, nie wyjątkiem - dlatego catchError zwraca of(...Failure).

Wybór operatora ma znaczenie - porównaj z wersją dla dodawania:

addSamurai$ = createEffect(() =>
  this.actions$.pipe(
    ofType(addSamurai),
    exhaustMap(({ samurai }) =>          // ignoruj kolejne, dopoki trwa
      this.http.post<Samurai>('/api/samurai', samurai).pipe(
        map(saved => addSamuraiSuccess({ samurai: saved })),
        catchError(error => of(addSamuraiFailure({ error: error.message })))
      )
    )
  )
);

Różnica to switchMap kontra exhaustMap. Przy wczytywaniu switchMap przerywa stare żądanie na rzecz nowego - liczy się tylko najnowszy wynik. Przy zapisie exhaustMap robi odwrotnie: ignoruje kolejne kliknięcia, dopóki trwa poprzednie - dzięki temu podwójne kliknięcie "Zapisz" nie utworzy dwóch samurajów. Zapamiętaj tę parę: switchMap do pobierania, exhaustMap do zapisu.

Pamiętaj z tej lekcji dwie role: selektor to okno, przez które czytasz wycinek stanu (i które samo się przelicza), a effect to most, który zamienia akcję na działanie w świecie i z powrotem w nową akcję. Store zostaje czysty, a cała rozmowa z serwerem płynie przez efekty.

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jaka jest główna zaleta createSelector() w NgRx?

  2. 2. Do czego służą Effects w NgRx?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Uzupełnij selektory dla filtrowania samurajów po klanie.

  • Układanie w poziomie

    Ułóż wywołanie createSelector z selektorem wejściowym i funkcją projekcji (jedna linia, od lewej do prawej):

  • Układanie w pionie

    Uporządkuj kroki wewnątrz Effectu loadSamurai$ (od nasłuchu akcji do wyniku):

  • Klikanie w kolejności

    Kliknij elementy w kolejności, aby zbudować początek strumienia w Effekcie nasłuchującym akcji:

Przydatne artykuły