Angular ngrx-store

Angularban a redux "megfelelője" az ngrx/store. Adatokat rakhatunk bele és olvashatjuk ki őket az alkalmazás bármelyik komponensében.

Store-ban redux-ban olyan adatokat célszerű tárolni, amelyek nem változnak sűrűn, de viszont szükségünk lehet rájuk az alkalmazás több pontján pl profil adatok beállítások stb.

Tegyük fel az @ngrx/store és @ngrx/effects csomagot. Majd az @ngrx/store-devtools csomagot. Ez utóbbi a chrome böngészőben levő redux-os devtool-os plugin-hoz szükséges, hogy megtudd nézni a böngészőben a store/state aktuális tartalmát. Ez a fejlesztésnél nagy segítség lehet.
https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd

Az angularban a redux-ot a következő részekből tudjuk megvalósítani: actions, effects, reduccers, services.

Az action-ökben deklaráljuk a redux-ban használható hívások azonosítóit(action type-okat) és definiáljuk azokat a függvényeket, amiket meghívunk majd a komponensekből.

Egy példa settings.actions.ts-re:
 
import { Action } from '@ngrx/store';

export const SETTING_BEGIN =    '[Setting] Load data begin';
export const SETTING_SUCCESS =    '[Setting] Load data success';
export const SETTING_FAILURE =    '[Setting] Load data failure';

export class LoadDataBegin implements Action {
    readonly type = SETTING_BEGIN;
}

export class LoadDataSuccess implements Action {
    readonly type = SETTING_SUCCESS;

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

export class LoadDataFailure implements Action {
    readonly type = SETTING_FAILURE;

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

/**
 * Export a type alias of all actions in this action group
 * so that reducers can easily compose action types
 */
export type SettingsActions
    = LoadDataBegin
    | LoadDataSuccess
    | LoadDataFailure;

A reducer-ben kezeljük a fent definiált action-öket az action-type-okkal. Bekerülnek a redux/store state-be az adatok vagy onnan olvassuk ki őket és adjuk vissza az action-öknek.
settings.reducer.ts példa fájl tartalma:
 
import * as Actions from '../actions/settings.action';

export interface SettingsDataState {
    list:  any;
    loading: boolean;
    error: any;
}

export const initialState: SettingsDataState = {
    list:  {},
    loading: false,
    error: null
};

export function settingReducer(state = initialState, action: Actions.SettingsActions) {
    switch (action.type) {

        case Actions.SETTING_BEGIN: {
            return {
                ...state,
                loading: true,
                error: null
            };
        }

        case Actions.SETTING_SUCCESS: {
            return {
                ...state,
                loading: false,
                list: action.payload.data
            };
        }

        case Actions.SETTING_FAILURE: {
            return {
                ...state,
                loading: false,
                error: action.payload.error
            };
        }

        default: {
            return state;
        }
    }
}

export const getSettings = (state: SettingsDataState) => state.list;

Az effect "érzékeli", hogy melyik action type-ot választottuk ki és meghívja a megfelelő service-t és actionöket.
Az settings.effect.ts fájl tartalma:
 
import {Injectable} from '@angular/core';
import {Effect, Actions, ofType} from '@ngrx/effects';
import * as SettingsActions from '../actions/settings.action';
import {SettingsService} from "../services/settings.service";
import {map, switchMap, catchError} from 'rxjs/operators';
import { of } from 'rxjs';

@Injectable()
export class SettingsEffect {
    constructor(private actions: Actions, private dataService: SettingsService) {
    }

    @Effect()
    loadData = this.actions.pipe(
        ofType(SettingsActions.SETTING_BEGIN),
        switchMap(() => {
            return this.dataService.getSettingListData().pipe(
                map(data => new SettingsActions.LoadDataSuccess({data: data})),
                catchError(error =>
                    of(new SettingsActions.LoadDataFailure({error: error.error}))
                )
            );
        })
    );
}

Service példa fájl, amit az effect hív meg és a reducceren keresztül bekerülnek az adatok a store-ba/redux-ba.
 
import {map} from 'rxjs/operators';
import {Injectable} from '@angular/core';
import {Setting} from '../models/settings.model';
import {HttpClient} from '@angular/common/http';
import {Base64} from 'js-base64';
import {of, Observable} from 'rxjs';
import {AppState, getSelectLocationDataState} from "../_config/reducers";
import {Store} from '@ngrx/store';

@Injectable()

export class SettingsService {

    protected settings: Setting[];
    location: any;

    constructor(private http: HttpClient, private store: Store<AppState>) {
        this.store.select(getSelectLocationDataState).subscribe(data => this.location = data);

        this.clear();
    }

    getSettingListData(): Observable<Setting[]> {

        if (this.location && this.location.id) {

            return this.http.get('/api/settings/' + this.location.id).pipe(map((r) => {
                return <Setting[]>r;
            }));
        }
        else {
            return of();
        }
    }

...
}
 
A komponensekben a this.store.dispatch -el tudjuk meghívni az action-öket és a this.store.select-el lehet lekérdezni a store/redux tartalmát. Ha az adatok a redux/store-ba való betöltődése után szeretnénk műveleltet végezni, akkor az actionSubject-et használhatjuk.
Példa komponensre:
 
import {Store, ActionsSubject} from '@ngrx/store';
import * as SettingAction from '../actions/settings.action';

import {
    AppState,
    getSettingsDataState
} from "../../../_config/reducers";

    ...

    settings: Settings[];

    constructor(private actionsSubject$: ActionsSubject, private store: Store<AppState>) {
        this.store.dispatch(new SettingAction.LoadDataBegin());
        this.store.select(getSettingsDataState).subscribe(data => this.settings = data);
    }

    ngOnInit() {
        this.actionsSubject = this.actionsSubject$.subscribe((action: Action) => {

            if (action.type === SettingAction.SETTING_SUCCESS) {

                  ...
            }
        });
    }

    ngOnDestroy() {
        if (this.actionsSubject) {
            this.actionsSubject.unsubscribe();
        }
    }

Én készítettem egy reducers és effects config fájlt, amibe felvehetem az összes reducer-t és effect-et. Ez jól fog jönni az app.module-be való importáláshoz.

reducers.ts:
 
import { ActionReducerMap, createSelector, MetaReducer} from "@ngrx/store";
import * as settingReducer from "../reducers/settings.reducer";

export interface AppState {
    settings: settingReducer.SettingsDataState;
}

export const reducers: ActionReducerMap<AppState> = {
    settings: settingReducer.settingReducer
};

export const metaReducers: MetaReducer<AppState>[] = [clearState];

export const getSettingsDataState = (state: AppState) => state.settings;

effect.ts:
 
import {SettingsEffect} from "../effects/settings.effect";

export const effects: any[] = [SettingsEffect];

Az app.module.ts-be a következő módon kell beállítanunk a létrehozott fájljainkat:
 
import {StoreModule} from '@ngrx/store';
import {EffectsModule} from '@ngrx/effects';
import {reducers, metaReducers} from "./_config/reducers";
import {effects} from "./_config/effects";

   ...

   imports: [
        ...
        StoreModule.forRoot(reducers, {
            runtimeChecks: {
                strictStateImmutability: true,
                strictActionImmutability: true,
                strictStateSerializability: true,
                strictActionSerializability: true,
            }, metaReducers
        }),
        EffectsModule.forRoot(effects),
        RouterModule.forRoot(appRoutes),


Olvasnivaló
https://ngrx.io/guide/store

https://github.com/ngrx/example-app

 
2020.07.11.