L’ottava versione di NgRx ha portato molti cambiamenti interessanti che ci consentono di scrivere codice più pulito e più snello. E c’è anche qualche chicca che vale la pena di scoprire! Il tutto con pochissimi breaking-change, quindi non avete più scuse: è quasi tutto retro-compatibile!

Le nuove Azioni – createAction

Prima della versione 8, ogni Azione era tipicamente rappresentata da una classe e si usava un enumerator per raggrupparne i type:

export enum TodoActionTypes {
  AddTodo = '[TODO] Add Todo';
  AddTodoSuccess = '[TODO] Add Todo Success';
  AddTodoFail = '[TODO] Add Todo Fail';
}

export class AddTodo implements Action {
  readonly type = TodoActionTypes.AddTodo;

  constructor(public payload: Todo) {}
}

export class AddTodoSuccess implements Action {
  readonly type = TodoActionTypes.AddTodoSuccess;

  constructor(public payload: Todo) {}
}

export class AddTodoFail implements Action {
  readonly type = TodoActionTypes.AddTodoFail;

  constructor(public payload: any) {}
}

export type TodoActions = AddTodo | AddTodoSuccess | AddTodoFail;

Ora diventa tutto più snello grazie alla nuova factory createAction():

import { createAction, props } from '@ngrx/store';

export const addTodo = createAction(
  '[TODO] Add Todo',
  props<{ todo: Todo }>()
);

export const addTodoSuccess = createAction(
  '[TODO] Add Todo Success',
  props<{ todo: Todo }>()
);

export const addTodoFail = createAction(
  '[TODO] Add Todo Fail',
  props<{ error: any }>()
);

Oltre ad essere più leggibile, risparmiamo circa un quarto delle linee di codice, riducendo il boilerplate.

Al posto di passare props() come secondo parametro (che semplicemente tipizza le proprietà dell’azione), possiamo anche specificare una nostra funzione se vogliamo aggiungere della logica: attenzione, è meglio che siano funzioni pure!

export const addTodoSuccess = createAction(
  '[TODO] Add Todo Success',
  ({ todo }: { todo: Todo }) => ({ ...todo, dateAdded: Date.now() })
);

La funzione createAction a sua volta genera una funzione: per creare l’azione vera e propria dobbiamo invocarla con i parametri giusti, e passarla al dispatch:

this.store.dispatch(addTodo({ todo: myTodo }));

I nuovi Reducer – createReducer

Anche i Reducer ottengono una ripulita: possiamo utilizzare la factory createReducer() per evitare di utilizzare degli switch che potrebbero diventare faticosi da maneggiare. Specifichiamo le clausole che prima erano all’interno dei case dello switch attraverso una nuova funzione, la funzione on().

import { createReducer, on } from '@ngrx/store';
import * as TodoActions from '../actions';

export const todoReducer = createReducer(
  initialState,
  on(TodoActions.AddTodo, (state, action) => ({ ...state, loading: true })),
  on(TodoActions.AddTodoFail, (state, action) => ({ ...state, loading: false })),
  on(TodoActions.AddTodoSuccess, (state, action) => ({ ...state, loading: false, todos: [ ...state.todos, action.todo ] }))
);

Attenzione: potete utilizzare tutti gli on() che volete, ma all’interno dello stesso reducer non ci possono essere più on() abbinati alla stessa azione! Se invece si tratta di reducer diversi, ovviamente non c’è nessun problema.

I nuovi Effetti – createEffect

Avevate qualche dubbio? Anche sugli Effect qualche lavoretto è stato fatto, ma c’è veramente poco di nuovo: al posto di utilizzare un decoratore, ora utilizziamo la nuova funzione createEffect(). Perché? Per motivi di sicurezza, infatti questa funzione ci darà un errore se l’effetto non dovesse ritornare un Observable.

// Prima...
@Effect()
addTodo$ = this.actions$.pipe(
  ofType(TodoActions.AddTodo),
  mergeMap(action => ...)
);

// Dopo...
addTodo$ = createEffect(() => 
  this.actions$.pipe(
    ofType(TodoActions.AddTodo),
    mergeMap(action => ...)
  )
);

Abbiamo ancora la possibilità di specificare che l’effetto non si aspetta un’azione di ritorno con dispatch: false e abbiamo anche una nuova opzione con cui possiamo dire a NgRx di non far ripartire l’Effect automaticamente in caso di errore:

addTodo$ = createEffect(() => 
  this.actions$.pipe(
    ...
  ),
  {
    dispatch: false,
    resubscribeOnError: false
  }
);

RuntimeChecks: deprecato ngrx-store-freeze

Se prima utilizzavate ngrx-store-freeze (cosa buona e giusta) per verificare di non far danni durante lo sviluppo e mantenere lo stato immutabile, ora è tutto integrato direttamente in StoreModule.forRoot().

@NgModule({
  imports: [
    StoreModule.forRoot(reducers, {
      runtimeChecks: {
        strictStateImmutability: true,
        strictActionImmutability: true,
        strictStateSerializability: true,
        strictActionSerializability: true,
      },
    }),
  ],
})
export class AppModule {}

Ecco cosa fanno le varie opzioni:

  • strictStateImmutability: dà un errore se modificate lo stato al di fuori da un reducer (ad esempio, se modificate un oggetto ritornato da un selector)
  • strictActionImmutability: dà un errore se modificate una qualsiasi azione dopo averla creata
  • strictStateSerializability: dà un errore se rileva che lo stato diventa non-serializzabile (ad esempio se ci inserite strutture circolari, come un Observable)
  • strictActionSerializability: dà un errore de rileva che una azione diventa non-serializzabile (stesso discorso)

Mock Selectors

Se testate le vostre applicazioni (dovreste farlo), sapete che se da un lato testare le componenti di NgRx è facile (reducer, azioni, selettori, effect un po’ meno), testare un componente che utilizza lo Store è un po’ più complesso.

Dalla versione 7 abbiamo MockStore, utilissimo per gli unit-test, che ci permette di manipolare il nostro stato a piacimento:

import { TestBed } from '@angular/core/testing';
import { Store } from '@ngrx/store';
import { provideMockStore, MockStore } from '@ngrx/store/testing';

// Nel beforeEach...
TestBed.configureTestingModule({
  providers: [
    provideMockStore(initialState)
  ],
});

store = TestBed.get<Store>(Store);

// E nei test...
store.setState(...);

Ma con la versione 8 abbiamo un’altra aggiunta: la possibilità di mockare i selettori con dei valori prestabiliti!

import { provideMockStore, MockStore } from '@ngrx/store/testing';

TestBed.configureTestingModule({
  providers: [
    provideMockStore({
      selectors: [
        { selector: fromTodos.selectTodos, value: [] },
        { selector: fromTodos.selectTodosCount, value: 0 },
        { selector: fromTodos.selectTodosLoading, value: false }
      ],
    }),
  ],
});

Possiamo anche fare l’ovverride volta per volta del valore del selettore:

store.overrideSelector(fromTodos.selectTodos, [ {...}, {...} ]);

Oppure, se non utilizziamo MockStore, possiamo comunque usare un nuovo metodo che è stato aggiunto direttamente ai selettori:

// Questo metodo ci ritorna il valore fra parentesi, ovvero l'array.
fromTodos.selectTodos.setResult([ {...}, {...} ]);

RouterState e Router Selectors

Vi ricordate che per poter serializzare lo stato del router avevate bisogno di un Serializer custom? Ora non ce n’è più bisogno, potete buttarlo nel cestino e utilizzare il nuovo RouterState.Minimal:

import { StoreRouterConnectingModule, RouterState } from '@ngrx/store';

StoreRouterConnectingModule.forRoot({
  routerState: RouterState.Minimal
});

E abbiamo anche dei nuovi selettori per le varie parti del router, quindi se li avete scritti a mano, buttate anche quelli! 😉

export const selectRouter = createFeatureSelector<
  State,
  fromRouter.RouterReducerState<any>
>('router');

export const {
  selectQueryParams,
  selectRouteParams,
  selectRouteData,
  selectUrl,
} = getSelectors(selectRouter);

Un nuovo pacchetto – @ngrx/data

Si tratta di una libreria precedentemente chiamata angular-ngrx-data che ora entra a far parte della famiglia @ngrx: è nata con lo scopo di semplificare la vita a chi deve lavorare con centinaia di entità e vuole evitare di scrivere centinaia di file da zero (reducer, actions, selectors, effects…). Sicuramente è una libreria interessante, ma personalmente sto consigliando ai miei clienti di aspettare ad usarla. Non è ancora molto matura (in alcuni casi, se si ha a che fare con API del server un po’ particolari, è difficile integrarla bene) e la documentazione è ancora in fase di scrittura, ma se vi piace smanettare o volete contribuire a migliorarla, provatela! Trovate la documentazione a questo link.

Conclusione

Mi fermo qui! Come avete visto, ci sono molte novità interessanti. Ho tralasciato qualcosina per non appesantirvi troppo la lettura (come i nuovi Schematic) e non ho parlato dei Breaking Changes: non preoccupatevi comunque, sono pochi e li trovate in questa pagina.

Vi ricordo infine che potete semplificarvi il processo di migrazione con la CLI, utilizzando ng update @ngrx/store 😉

Ti è piaciuto l’articolo? Condividilo!

Rilasciare risorse gratuite non è facile: un sacco di tempo vola fra la ricerca dell’idea, la stesura, la preparazione di codice funzionante e la revisione. Se ti piace ciò che facciamo, condividi l’articolo: nel peggiore dei casi ti daranno del secchione!

Ti interesserà anche…

Angular

Creazione di Guardie per i Moduli

I moduli sono una delle feature che più amo di Angular: ci permettono di racchiudere componenti, servizi, pipe e direttive all’interno di un unico contenitore. Ma non si tratta soltanto…
Angular

CoreModule contro providedIn

Da tempo siamo abituati a creare nelle nostre applicazioni Angular un modulo specifico: CoreModule, ovvero il modulo che “sgrassa” AppModule consentendoci una maggiore pulizia. Ma ha ancora senso usarlo, ora…

Iscriviti alla nostra newsletter!

Rimani aggiornato per quando rilasceremo nuovi articoli, nuovi corsinuove promozioni e nuove risorse gratuite. Ti promettiamo che useremo la tua email con prudenza, e non la condivideremo con nessuno senza il tuo consenso!

Abbiamo a cuore la tua privacy. Niente spam. Leggi la nostra privacy policy qui.

Menu