06 paź 2022
5 min

Co nowego w NgRx? Przegląd zmian i praktyczne wskazówki.

Zakładam, że słyszeliście już  o State Managemencie – do czego służy, dlaczego powinniśmy go stosować i w czym może nam pomóc. Jak chcecie odświeżyć swoją wiedzę, to polecam zajrzeć do nagrania prelekcji Mateusza Do Duca z Angular Meetup.

My, w ramach tego artykułu zajmiemy się NgRx – przejrzymy kilka nowości i sposobów na ułatwienie sobie z nim pracy. 

Artykuł jest aktualizacją starszego artykułu o NgRx: https://www.angular.love/2019/02/27/ngrx-praktycznie-garsc-wskazowek/

Akcje

Każdy, kto korzystał kiedyś ze state managementu od NgRxa, na pewno dobrze wie czym są i do czego służą akcje. Zasada działania pozostaje taka sama pomiędzy wersjami, więc co w takim razie zmieniło się w samych akcjach? Przede wszystkim składnia, jak w całym NgRxie 🙂 W tym momencie mamy dwie drogi do tworzenia akcji.

Pierwsza to używanie metody createAction:

export const login = createAction(
  '[Login Page] Login'
  props<{ payload: LoginPayload }>()
);

W porównaniu z poprzednim sposobem, czyli klasa z konstruktorem, jest prościej. 

export class Login implements Action {
  readonly type = '[Login Page] Login'

  constructor(public payload: LoginPayload){}
}

Mniej boilerplate’u i wygląda dużo ładniej, ale czy jest to sposób doskonały? Nie. Pewnie nie raz mieliście sytuacje kiedy metodą copiego pasta tworzyliście nowe akcje i zapomnieliście zmienić typ akcji. Typy muszą być unikalne. W przeciwnym razie, może to spowodować nieoczekiwany rezultat, na przykład podwójny call do API. Tutaj z pomocą przychodzi nam zupełnie nowy sposób createActionGroup:

const authApiActions = createActionGroup({
  source: 'Auth API',
  events: {
    'Login': props<{ payload: LoginPayload }>(),
    'Login Success': props<{ userId: number; token: string; }>(),
    'Login Failure': props<{ error: string; }>(),
  },
});

Sposób, który pozbawia nas problemów – events jest recordem, nasz typ jest kluczem i nie możemy dać dwóch akcji z takim samym kluczem. Cudo.

concatLatestFrom 

Do tej pory, jeśli chcieliśmy w efekcie wyciągnąć dane ze state’u, zawsze korzystaliśmy z operatora withLatestFrom. W tej chwili team NgRxa zaleca używane nowego – concatLatestFrom. Jaka jest pomiędzy nimi różnica?

Przy operacji wyciągania danych ze state’u czasami mogliśmy się spotkać z sytuacją kiedy otrzymujemy nieaktualne dane. Wtedy przychodzi nam na myśl, że coś musi dziać się za szybko. W naszym przypadku za szybkie jest właśnie wyciąganie danych ze statu.

W withLatestFrom subskrybcja była “eager”, czyli mogła nasłuchiwać na akcję jeszcze zanim ona została wykonana w efekcie. W nowym operatorze concatLatestFrom subskrybcja jest “lazy”, to znaczy, że akcja zacznie być nasłuchiwana dopiero wtedy gdy wykona się w efekcie. ConcatLatestFrom eliminuje ten błąd i pobiera dane dopiero po zdispatchowaniu akcji.

Entities

Dobrą praktyką w zarządzaniu danymi jest przechowywanie jako obiekty z kluczem, czyli mapa. Świat stał się piękniejszy kiedy team NgRxa stworzył rozwiązanie, które nam to zapewnia out of the box. Wystarczy tylko użyć @ngrx/entitiy. Używanie @ngrx/entitiy do przechowywania danych daje nam również benefit w postaci szybkości odczytu danych. Złożoność obliczniowa operacji pobrania danych z mapa wynosie O(1) w porówaniu do O(n) w przypadku tablicy. 

State tworzony za pomocą entity wygląda tak:

export interface State extends EntityState<User> {
  selectedUserId: string | null;
}

export const adapter: EntityAdapter<User> = createEntityAdapter<User>({
  selectId: (user: User) => user.userId
});

export const initialState: State = adapter.getInitialState({
  selectedUserId: null,
})

Nasz interface state’u rozszerzamy przez EntityState, a nasz inicjalny stan tworzymy za pomocą entityAdaptera.

Rezultatem jest stworzony state, który oprócz zadeklarowanych przez nas pól, ma dodatkowe pola: entites i ids. Entities jest mapą obiektów, które chcemy w nim trzymać. Domyślnie kluczem jest props id. Możemy oczywiście go zmienić. W tym przypadku selectUserId będzie naszym kluczem. Dlaczego wspominam o domyślnym kluczu i możliwości jego zaminy? Przyjmijmy, że nasz model usera wygląda tak:

export interface User {
  userId: number;
  name: string;
  surname: string;
}

W takim przypadku, jeśli ustawimy dane tak jak przychodzą z API do naszego statu zobaczymy, że dane nie ustawiły sie prawidłowo.

W entites pierwszy klucz jest undefined właśnie dlatego, że nie ustawiliśmy innego klucza dla mapa. W takim wypadku musimy albo przemapowac nasze dane i dodać do nich dodatkowy props (id) lub zmienić domyślmy klucz entites z id w naszym przypadku na userId. 

Czym tak właściwie jest ten adapter? Adapter udostępnia nam metody do modyfikacji entities. Czyli za jego pomocą możemy coś do entities dodać, usunąć, zmodyfikować i wiele innych.

export const userReducer = createReducer(
  initialState,
  on(UserActions.loadUsers, (state, { users }) => {
    return adapter.setAll(users, state);
  }),
  on(UserActions.updateUser, (state, { update }) => {
    return adapter.updateOne(update, state);
  }),
  on(UserActions.deleteUser, (state, { id }) => {
    return adapter.removeOne(id, state);
  }),
);

Selektory

Selektory z props są w tym momencie deprecated. Prosty przykład jak wyglądał selektor z propsem, a jak powinien wyglądać w tym momencie.

// correct
export const getCount = (multiply: number) => createSelector( getCounterValue, (counter) => counter * multiply );

// depricated
export const getCount = createSelector( getCounterValue, (counter, props) => counter * props.multiply );

Zmienił się też zalecany sposób wywoływania selektorów. Teraz wygląda to dużo bardziej przyjaźnie.

// use that 
this.store.select(selectUser());

// instead of
this.store.pipe(select(selectUser));

NgRx a standalone components

Wersja 14 angulara przyniosła sporo zmian, w tym długo wyczekiwane podejście standalone. W tym momencie możemy stworzyć naszą aplikację bez użycia modułów. Jak w takim razie zadeklarować reducer i effecty? 

Jeśli chcemy zadeklarować nasz state lub efekt na globalnym poziomie możemy to zrobić za pomocą metody importProviderFrom w application injectorze.  

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(
      StoreModule.forRoot({
        router: routerReducer,
        auth: authReducer,
      }),
      StoreRouterConnectingModule.forRoot(),
      StoreDevtoolsModule.instrument(),
      EffectsModule.forRoot([RouterEffects, AuthEffects])
    ),
  ],
});

Ngrx przygotował również swoje dedykowane metody do importowania state i effectów: provideEffect i provideStore.

bootstrapApplication(AppComponent, {
  providers: [
    provideStore({ router: routerReducer, auth: AuthReducer }),
    provideRouterStore(),
    provideStoreDevtools(),
    provideEffects([RouterEffects, AuthEffects]),
  ]),
});

Zamiennik importu forFeature jest równie prosty. State i efekty możemy provide’ować w routingu.

path: '',
providers: [
  provideStoreFeature('users', usersReducer),
  provideFeatureEffects([UsersApiEffects]),
],
children: [
...
]

Podsumowując, na przestrzeni lat w NgRx’ie dochodzi całkiem sporo nowych rozwiązań, usprawnień które ułatwiają pracę nam – deweloperom. Nie ma wątpliwości, że NgRx jest najbardziej popularnym i najczęściej spotykanym state managementem w projektach angularowych więc warto być na bieżąco, zawsze aktualizować paczki i zaglądać na naszego bloga, gdzie regularnie informujemy was o tipach i nowościach, które są wydawane.

Podziel się artykułem

Zapisz się na nasz newsletter

Dołącz do community Angular.love i bądź na bieżąco z trendami.