Utilizzare Redux in applicazioni Angular con la libreria NgRx

di Morgan Pizzini, in Angular,
  • 0
  • 0
  • 0
  • 110,03 KB

Con l'avvento dei framework Client side, e principalmente grazie a React, è sorta la necessità di salvare i dati scaricati dal Web Server per poterli poi riutilizzare in più punti dell'applicazione, eseguire operazioni di aggiornamento, diramare l'effetto dell'aggiornamento e visualizzare immediatamente queste modifiche in tutte le aree dell'applicazione. L'insieme di tutti i meccanismi che forniscono queste funzionalità vengono chiamati State management.

State management

Le prime versioni di React sono state implementate su di una architettura chiamata Flux, trasformatosi poi in Redux: Flux + Functional programming, questa evoluzione è dovuta alla natura 'one-way' della libreria Flux i cui oggetti e meccanismi interni di distribuzione aggiornamenti non combaciavano perfettamente con React, ma procediamo con calma...

Lo state management pattern di compone di 3 elementi cardini Action, Store e Reducer.

  • Con Actions si possono identificare l'insieme delle classi che consentono di eseguire richieste verso lo store. Dalla richiesta di ottenere una lista, al salvataggio di un entità, o l'intenzione di voler rimuovere dati.
  • Lo Store può essere definito come un database in memoria, questo è univoco per tutta l'applicazione, ma può contenere al suo interno altri store generati al suo interno. Le uniche entità preposte per la modifica dello store sono i Reducers.
  • Un Reducer è una funzione funzione pura, accetta cioè in ingresso l'elemento store che poi restituirà al termine della funzione. Il compito del reducer è modificare lo store sulla base dell'azione invocata e dai parametri inseriti nell'azione stessa.

Premessa: ambiti di utilizzo in Angular

All'interno di React il Redux pattern è alla base del sistema di comunicazione tra le varie views, ma come sappiamo il framework Angular ha un buon livello di complessità ed è strutturato su vari layer: componenti, direttive, servizi, interceptor ecc.. Inserire altra complessità utilizzando uno store management potrebbe non essere una buona scelta architetturale e impone al team, o a chiunque andrà poi a manuntenere il codice, di conoscere questo pattern di programmazione. Può essere utile analizzare in anticipo quando possiamo trarre benefici da questo pattern.

E' sicuramente da evitare quando:

  • Il workflow dall'utente dell'applicazione non è complesso.
  • Non bisogna gestire alcun server side event (SSE) o websockets.
  • Ogni componente utilizza i dati recuperati da una specifica data source creata appositamente per il componente.

Di contro è consigliato quando:

  • L'utente, utilizzando l'applicazione, interagisce con molti componenti/views.
  • L'applicazione implementa la gestione di SSE, come socket.io o SignalR
  • I dati visualizzati da un componente possono provenire da diverse fonti, o diversi componenti devono visualizzare gli stessi dati. Es. il nome utente può essere visualizzato in più punti del layout da diversi componenti.

Redux in Angular

Per poter ricostruire il Redux pattern avremo bisogno di un libreria: NgRx.La gestione dello store e la comunicazioni tra le parti è stata creata tramite RxJS ed è quindi pienamente compatibile ed interagibile tramite Angular. Rispetta appieno i cardini del Redux pattern apportando qualche modifica che semplifica il ruolo del developer:

  • I Reducers sono funzioni pure ma, oltre ad avere in ingresso lo state corrente, richiedono anche l'ultima action eseguita. Questo per poter modificare lo state sulla base della richiesta effettuata.
  • Vengono introdotti i Selectors, anch'essi funzioni pure che vengono utilizzate per derivare e comporre parti di state.
  • Tutto ciò che viene fornito dallo state management è accessibile tramite uno Store.
Installazione

Come tutti i pacchetti npm l'installazione può avvenire tramite il comando

npm install @ngrx/store --save

se il progetto utilizza una CLI versione 6+ possiamo usare la funzionalità ng per installare ed inserire all'interno dell'app.module.ts gli import necessari.

ng add @ngrx/store

per la demo che svilupperemo in questo articolo avremo anche bisogno di queste librerie

npm install @ngrx/effects @ngrx/entity --save

che rispettivamente consentiranno di modificare lo state sulla base di eventi esterni e ci consentiranno di avere degli helper per creare/modificare/ottenere le entità salvate nello state.

Esiste anche un set di utilities che ci possono tornare utili durante lo sviluppo, installabili tramite

npm install @ngrx/store-devtools --save

NgRx fornisce anche un set di schematics per la creazione dei boilerplace, questi dovranno, dopo essere stati scaricati, impostati come schematics di default del progetto Angular

npm install @ngrx/schematics --save-dev
ng config cli.defaultCollection @ngrx/schematics

Gli schematics classici di angular non verranno persi perchè @ngrx/schematics estende @schematics/angular. Per controprova, nel file angular.json dovremmo avere questa sezione

"schematics": {
  "@ngrx/schematics:component": {
    "styleext": "scss"
  }
}

Architettura applicazione

Procediamo alla creazione di un modello per la nostra applicazione

export interface Book {
  bookId: string;
  title: string;
  description: string;
}
Action

Le action sono gli eventi che avvengono nella nostra applicazione. Nel caso di una pagina in cui dobbiamo caricare una lista di libri, una action può essere load books, verrà quindi eseguito un dispatch dell'azione.

E' importante specificare che ogni azione sarà responsabile di un possibile cambio di state, quindi l'azione di load book potrà impostare una proprietà loading a true all'interno dello state, allo stesso modo, quando il load sarà terminato verrà scatenata un'azione, load book success che inserirà i libri caricati nello store e imposterà la proprietà loading a false.

Da questa descrizione si può evincere come ogni action, il cui ruolo sarà interrogare un servizio, avrà due azioni correlate una success ed una error/fail.

Incominciamo a strutturare le action partendo dalla definizione dei tipi di azione all'interno di un enumeratore.

export enum BookActionTypes {
  LOAD_BOOKS = '[Book] Load books',
  LOAD_BOOKS_SUCCESS = '[Book] Load books success',
  LOAD_BOOKS_ERROR = '[Book] Load books error'
}

I valori inseriti nell'enumeratore verranno usati come identificatori univoci dell'azione, sarà quindi nostra premura verificare che non vi siano mai due azioni con lo stesso valore.

Proseguiamo inserendo le azioni. Queste sono delle classi che implementano l'interfaccia Action di @ngrx/store, hanno una proprietà che definisce il tipo di azione, e possono avere dei parametri nel costruttore che per convenzione vengono wrappati in una proprietà definita payload.

export class LoadBooks implements Action {
  readonly type = BookActionTypes.LOAD_BOOKS;

  constructor() { }
}

export class LoadBooksSuccess implements Action {
  readonly type = BookActionTypes.LOAD_BOOKS_SUCCESS;

  constructor(public payload: { books: Book[] }) { }
}

export class LoadBooksError implements Action {
  readonly type = BookActionTypes.LOAD_BOOKS_ERROR;

  constructor(public payload: { error: any }) { }
}

Per poter utilizzare il type inference nel reducer sarà necessario creare un tipo che conterrà l'unione di tutte le action presenti.

export type BookActionsUnion =
  | LoadBooks
  | LoadBooksSuccess
  | LoadBooksError;

Il codice completo è disponibile nell'allegato a questo articolo.

Reducer

Come già anticipato, all'interno del reducer andremo ad implementare tutti i cambiamenti che deve subire lo state a seguito di un dispatch. Dato che con il Redux pattern vi è la possibilità di avere più state che verranno convogliati in uno più grande, possiamo definire uno state apposito per i libri e scrivere un reducer ad-hoc.

La prima fase sarà quella di creare un modello per lo state e definirne il valore di default.

export interface State {
  isLoading: boolean;
}

export const initialState: State = {
  isLoading: false
};

Successivamente possiamo definire un reducer che, basandosi su questo state, possa modificarlo a seconda dell'azione.

export function reducer(state = initialState, action: BookActionsUnion): State {
  switch (action.type) {
    case BookActionTypes.LOAD_BOOKS: {
      return {
        ...state,
        isLoading: true
      };
    }
    case BookActionTypes.LOAD_BOOKS_SUCCESS: {
      return {
        ...state,
        isLoading: false
      };
    }
    case BookActionTypes.LOAD_BOOKS_ERROR: {
      return {
        ...state,
        isLoading: false
      };
    }
    default:
      return state;
  }
}

In questo esempio all'interno del case LOAD_BOOKS_SUCCESS avremmo dovuto utilizzare una proprietà books all'interno del payload, al contempo, all'interno dello state avremmo dovuto avere una proprietà books: Book[] per il savataggio dei libri proveniente dal servizio. Ciò non è necessario perchè in NgRx abbiamo a disposizione un pacchetto (@ngrx/entity) che conterrà tutti gli helper e le ottimizzazioni necessarie per eseguire operazioni su liste di oggetti.

Il codice completo è disponibile nell'allegato a questo articolo.

2 pagine in totale: 1 2

Attenzione: Questo articolo contiene un allegato.

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