Le novità della versione 8 della libreria NgRx per utilizzare Redux in applicazioni Angular

di Morgan Pizzini, in Angular,

In un precedente articolo abbiamo già parlato di NgRx e delle sue implementazioni. Ma, parallelamente alle grandi novità presentate con la nuova versione di Angular 8, anche la più utilizzata libreria di state management ha subito grossi cambiamenti e nuove funzionalità.

Il target della versione 8, si può dire sia ridurre la verbosità dell'architettura e permettere di integrare agilmente la libreria consentendo allo sviluppatore di concentrarsi sulle funzionalità applicative e non sul classico copia/incolla per creare nuove actions/reducers/states/effects.

@ngrx/data

La prima grande novità è l'integrazione, all'interno della documentazione ufficiale, della libreria @ngrx/data (sviluppata da John Papa e Ward Bell), la quale consente di astrarsi dalla struttura di NgRx. @ngrx/data è una libreria basata su NgRx, quindi il suo utilizzo non va ad alterare l'infrastruttura del progetto; grazie a questo design, è possibile mixare l'utilizzo "standard" con quello fornito dalla libreria, facendo interagire vari stati, anche se instanziati con metodi diversi.

@ngrx/data consente di automatizzare la creazione di action, reducer, effect, dispatcher e selector per una determinata entità. Per ogni entità predispone una serie di chiamate HTTP per le operazioni più comuni: lettura, creazione, modifica, eliminazione. Out-of-the-box supporta il salvataggio in modalità ottimistica o pessimista e il salvataggio transazionale sulle varie entità. A tutto questo si somma la possibiltà di sovrascrivere ogni singola impostazione di default, garantendo un ottimo livello di flessibilità e adattabilità all'integrazione nei vari progetti.

I tempi di startup sono veramente minimi: per definire delle entità dobbiamo creare una constante che erediti dalla classe base EntityMetadataMap.

entityMetadata: EntityMetadataMap = {
  Hero: {},
  Villain: {}
};

L'implementazione di default costruirà action/state/service/reducer per le entity specificate. Poiché sulle operazioni che lavorano su entità multiple potrebbero esserci typo dovuti all'irregolarità delle parole plurali, possiamo effettuare un override con il seguente codice che specifica i nomi plurali.

const pluralNames = { Hero: 'Heroes' };

Per ultimare l'installazione, procediamo a esportare la nostra configurazione, che utilizzeremo all'interno dell'AppModule.

export const entityConfig = {
  entityMetadata,
  pluralNames
};

...

imports: [
  StoreModule.forRoot({}),
  EffectsModule.forRoot([]),
  EntityDataModule.forRoot(entityConfig)
]

Per poter ottenere le entità attraverso le API lato server, dobbiamo creare un service che, come per gli store, effettuerà le chiamate per noi basandosi su una configurazione di default ed esporrà i selector per accedere alle entità all'interno dello stato.

@Injectable({ providedIn: 'root' })
export class HeroService extends EntityCollectionServiceBase<Hero> {
  constructor(serviceElementsFactory: EntityCollectionServiceElementsFactory) {
    super('Hero', serviceElementsFactory);
  }
}

Per l'utilizzo all'interno di un componente basterà importare il service appena creato ed utilizzarne le proprietà ed i metodi esposti.

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.scss']
})
export class HeroesComponent implements OnInit {
  loading$: Observable<boolean>;
  heroes$: Observable<Hero[]>;

  constructor(private heroService: HeroService) {
    this.heroes$ = heroService.entities$;
    this.loading$ = heroService.loading$;
  }

  ngOnInit() {
    this.getHeroes();
  }

  add(hero: Hero) {
    this.heroService.add(hero);
  }

  delete(hero: Hero) {
    this.heroService.delete(hero.id);
  }

  getHeroes() {
    this.heroService.getAll();
  }

  update(hero: Hero) {
    this.heroService.update(hero);
  }
}

@ngrx/store

@ngrx/store è la libreria principale che contiene action/reducer/selector/store.

Action

Le actions, all'interno di uno state managment, sono le richieste di esecuzione di operazioni fatte dall'applicazione verso lo state stesso. La loro interfaccia, ed utilizzo è rimasto invariato. L'interfaccia Action implementa solo la proprietà type che rappresenta il tipo dell'action. Tutte le action che si andranno a definire potranno avere oltre al tipo anche delle proprietà che definiscono l'action stessa.

La differenza, rispetto alle versioni precedenti di NgRx sta nella dichiarazione. fino alla versione 7, per dichiarare un'azione e poterla utilizzare nell'applicazione avevamo bisogno di codice come quello del prossimo esempio, per poter garantire la tipizzazione e scrivere codice type-safe.

export enum HeroActionTypes {
  GET_HERO= '[HEROES] Get hero'
  GET_HERO_SUCCESS= '[HEROES] Get hero success'
}

export class GetHero implements Action {
  readonly type = HeroActionTypes.GET_HERO;
  constructor(public payload: { id: string}) { }
}

export class GetHeroSuccess implements Action {
  readonly type = HeroActionTypes.GET_HERO_SUCCESS;
  constructor(public payload: { hero: Hero }){ }
}

export type HeroActionsUnion = GetHero | GetHeroSuccess;

Con la versione 8, possiamo utilizzare un metodo che ci consentirà di creare le actions più rapidamente mantenendo invariato il controllo sulla tipizzazione.

export const GetHero = createAction (
  '[HEROES] Get hero',
  props<{ id : string }>()
);

export const GetHeroSuccess = createAction (
  '[HEROES] Get hero success',
  props<{ hero : Hero }>()
);

L'utilizzo, come anticipato, è rimasto invariato dalla versione 7 alla 8.

getHero(heroId: string){
  this.store.dispatch(GetHero({ id: HeroId}));
}
Reducer

Dati i cambiamenti alle actions, anche per i reducer si è intervenuto per pulirne il codice, renderlo più leggibile e facilmente configurabile. La novità principale è data dalla rimozione dello switch, per identificare la action eseguita, in favore di una sintassi più pipe-friendly. Inoltre il reducer, che fino ad ora era dichiarato come una funzione, è possibile dichiararlo come una costante il cui valore è la risultante della funzione createReducer.

Per capire meglio cosa significhi questo cambiamento, analizziamo questo codice tipico della versione 7.

import * as fromHeroes from '../actions/hero.actions';

export interface State {
  heroes: Hero[];
  isLoading: boolean;
}

export const initialState: State = {
  heroes: [],
  isLoading: false
};

export function reducer(state= initialState, action: fromHeroes.HeroActionsUnion){
  switch(action.type){
   case fromHeroes.LOAD_HEROES_SUCCESS:
     return {
       ...state,
       heroes: action.payload,
       isLoading: false
     };
    default:
      return state;
  }
}

Questo codice è estremamente verboso e all'incrementare delle action può diventare anche poco leggibile. A partire dalla versione 8 abbiamo la possibilità di definire il reducer molto più concisamente aumentando la leggibilità e quindi la manutenibilità

import * as fromHeroes from '../actions/hero.actions';

export interface State {
  heroes: Hero[];
  isLoading: boolean;
}

export const initialState: State = {
  heroes: [],
  isLoading: false
}

export cont reducer = createReducer(
  initialState,
  on(fromHeroes.loadHeroesSuccess, (state, { heroes }) => ({
    ...state,
    heroes: heroes,
    loading:false
  })
  )
)

Il codice così scritto appare da subito più leggibile e potente. Innanzitutto, non abbiamo più bisogno della dichiarazione di un default per lo switch. Poi possiamo aggregare più azioni semplicemente passandole come parametri al metodo on prima dell'arrow function. Infine utilizzando le arrow function i reducer permettono più facilmente di scrivere logica.

Runtime checks

Nella precedente versione, avevamo la possibilià, tramite il pacchetto ngrx-store-freeze, di eseguire dei runtime checks per aiutare a configurare correttamente lo state management e verificare che ciò che scriviamo rispetti l'infrastruttura. Con la versione 8, questi check sono stati aggiunti alla libreria ufficiale.

...
imports: [
    StoreModule.forRoot(reducers, {
      runtimeChecks: {
        strictStateImmutability: true,
        strictActionImmutability: true,
        strictStateSerializability: true,
        strictActionSerializability: true,
      },
    }),
  ],
...

La proprietà strictStateImmutability controlla che una proprietà dello state non venga mai modificata direttamente, ma che le modifiche allo state avvengano tramite la sovrascrittura di tutto lo state.

state.heroes = []; //ERRORE 
return {...state, heroes: [] } //CORRETTO

La proprietà strictActionImmutability è simile alla precedente, ma controlla l'immutabilità del valore ricevuto dalle action.

(state, {hero}) => { hero.id=null; return { ...state, hero: hero } }; //ERRORE

La proprietà strictStateSerializability specifica che lo state dovrà essere serializzabile, per poter consentirne il riutilizzo futuro.

{ data: new Date() } //ERRORE
{ data: new Date().toJSON() } //CORRETTO

In questo caso, la data serializzata come stringa è corretta in quanto il tipo Date non è considerato serializzabile.

La proprietà strictActionSerializability specifica che, come per lo state, anche le action devono essere serializzabili per essere riutilizzabili.

const createHero = createAction('[Heroes] Add new hero', hero => ({
  hero,
  // una funzione come parametro in ingresso, non è serializzabile
  logHero: () => {
    console.log(hero)
  },
}))
2 pagine in totale: 1 2
Contenuti dell'articolo

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

Top Ten Articoli

Articoli via e-mail

Iscriviti alla nostra newsletter nuoviarticoli per ricevere via e-mail le notifiche!

In primo piano

I più letti di oggi

In evidenza

Misc