Manage state with @ngrx/store
Prerequisites
If you don't have installed the Angular CLI do it now:
npm i -g @angular/cli
If and only if you don't have installed the CLI and the last command didn't work (e. g. b/c of your firewall) you can use the project-local CLI installation here. In this case, you have to execute the CLI with npm run. The next snipped shows this by requesting the CLI's version:
npm run ng -- -v
Please note, that you need those two dashes to tell npm that the parameters are not indented for npm but ng.
If you have a newer version of npm, you could also use npx:
npx ng -v
Setup the store
-
Open your
package.jsonand find out, that some libraries from the@ngrx/*scope have been installed. One of them is@ngrx/schematicswhich extends the CLI by additional commands we are using in the next steps to generate boilerplate code. -
To setup the
StoreModuleand all the needed imports, switch into the folderflight-app\src\appand run the following command.ng generate @ngrx/schematics:store AppState --root --statePath=+state --module=app.module.ts --project=flight-app -
Open the new
+statefolder and itsindex.tsfile. -
Open the
app.module.tsfile and inspect the current changes. You should find some additional imported modules.Check whether all
importstatements in this file work. If not, correct them (sometimes the generated code has some minor issues). -
Also import the
EffectsModuleinto theAppModule. Even though we will use it only later, we have to import it now to make the generated code run.import { EffectsModule } from '@ngrx/effects'; [...] imports: [ [...], EffectsModule.forRoot([]) ];
Setup State Management for a Feature Module
-
To setup the
StoreModulefor a feature module, switch into the folderflight-app\src\appand use the following command:ng generate @ngrx/schematics:feature flight-booking/+state/flight-booking --module=flight-booking/flight-booking.module.ts --creators --apiOpen the new
+statefolder and inspect the files.Inspect all of them and take a look at the
flight-booking.module.tswhere everything is imported. See that the.forFeaturemethod is called here. -
Open the file
flight-booking.effects.tsand remove the body of the classFlightBookingEffectsas well as all unnecessary imports. Will will come back to this file in a later exercise.import {Injectable} from '@angular/core'; // No other imports, for now @Injectable() export class FlightBookingEffects { // No body, for now } -
Open the file
flights-booking.reducer.ts. Extend the interfaceStateby a propertyflightswith the typeFlight[].Show code
export interface State { flights: Flight[] } -
Below, define an empty array as the initial state for the new property
initialState.Show code
export const initialState: State = { flights: [] }; -
In the same file, insert an interface
FlightBookingAppStatethat represents the root nodes view to our State:export interface FlightBookingAppState { flightBooking: State } -
Open the file
flight-booking.actions.tsand setup aflightsLoadedaction creator.Show code
You can replace the whole file with the following content:
[...] export const flightsLoaded = createAction( '[FlightBooking] FlightsLoaded', props<{flights: Flight[]}>() ); -
Open the file
flight-booking.reducer.tsand extend the reducer function so that it handles theflightsLoadedaction.Show code
export const flightBookingReducer = createReducer( initialState, on(flightsLoaded, (state, action) => { const flights = action.flights; return { ...state, flights }; }) ) -
Open the file
flight-search.component.ts. Inject the Store into the constructor. Introduce a propertyflights$(Observable<Flight[]>) which points to the flights in the store.Show code
export class FlightSearchComponent implements OnInit { [...] flights$ = this.store.select(s => s.flightBooking.flights); constructor( [...] private store: Store<FlightBookingAppState> ) { } [...] } -
Modify the component's
searchmethod so that the loaded flights are send to the store. For this, use theFlightService'sfindmethod instead of theloadmethod and dispatch aflightsLoadedaction.Show code
search(): void { if (!this.from || !this.to) return; // old: // this.flightService.load(...) // new: this.flightService .find(this.from, this.to, this.urgent) .subscribe({ next: flights => { this.store.dispatch(flightsLoaded({flights})); }, error: error => { console.error('error', error); } }); } -
Open the component's template,
flight-search.component.html, and use the observableflights$together with theasyncpipe instead of the arrayflights.Show code
<div *ngFor="let f of flights$ | async"> <div class="col-xs-12 col-sm-6 col-md-4 col-lg-3"> <flight-card [...]></flight-card> </div> </div> -
Test your solution
-
If not already installed, install the Chrome plugin
Redux DevToolsand use it to trace the dispatched actions.To install it, use Chrome to navigate to this page.
Update a Flight
In this exercise you will write an action for updating a flight. You will use it to delay the first flight by 15 minutes.
This exercise shows that working with immutables in JavaScript is not always as straight we would like it to be.
-
Open the file
flight-booking.actions.tsand add aupdateFlightaction creator for updating a changed flight in the store.Show code
[...] export const updateFlight = createAction( '[FlightBooking] Update Flight', props<{flight: Flight}>() ); -
Open the file
flight-booking.reducer.tsand update the reducer to handle theFlightUpdateAction.Show code
[...] const flightBookingReducer = createReducer( initialState, on(flightsLoaded, (state, action) => { const flights = action.flights; return { ...state, flights }; }), // New: on(updateFlight, (state, action) => { const flight = action.flight; const flights = state.flights.map(f => f.id === flight.id? flight: f); return { ...state, flights }; }) ); [...] -
Open the
flight-search.component.ts. Within thedelaymethod, dispatch aFlightUpdateActionthat delays the first found flight for 15 minutes. As the flight is immutable, you have to create a new one.Show code
delay(): void { this.flights$.pipe(take(1)).subscribe(flights => { const flight = flights[0]; const oldDate = new Date(flight.date); const newDate = new Date(oldDate.getTime() + 15 * 60 * 1000); const newFlight = { ...flight, date: newDate.toISOString() }; this.store.dispatch(updateFlight({flight: newFlight})); }); } -
Open the
flight-search.component.htmlfile and make sure that the theDelaybutton uses theflights$observable instead of the flights array. A very simple way to accomplish this is using theasyncpipe:<ng-container *ngIf="flights$ | async as flights"> <button *ngIf="flights.length > 0" class="btn btn-default" (click)="delay()"> Delay 1st Flight </button> </ng-container>If there is time, your instructor will discuss alternatives for this with you.
-
Test your solution.
Bonus: Connecting the Router to the Store **
The StoreRouterConnectingModule helps to sync your router with the store. This exercise shows how to use it.
-
Open your file
flight-app/src/app/+state/index.ts, import therouterReducerfrom@ngrx/router-storeand register it:import { routerReducer } from '@ngrx/router-store'; [...] export const reducers: ActionReducerMap<State> = { router: routerReducer }; -
In your
app.module, configure theStoreRouterConnectingModuleas shown below:import { RouterState } from '@ngrx/router-store'; [...] @NgModule({ imports: [ [...] StoreRouterConnectingModule.forRoot({stateKey: 'router', routerState: RouterState.Minimal }) ], [...] }) export class AppModule { } -
Start your application and navigate between the menu points. Open the Redux Devtools and replay all actions. Your should see, that the visited routes are replayed too.
Bonus: Using Mutables with ngrx-immer *
The project ngrx-immer uses the library immer to allow mutating the state. At least, it looks like this. However, your mutations are only recorded and replayed in an immutable way. As a result, you can write your code as you are used to and although you get the benefits of immutables.
You can quickly try this out:
- Install ngrx-immer:
npm i ngrx-immer immer - Lookup its readme: https://www.npmjs.com/package/ngrx-immer
- Update your reducer so that it uses
immerOninstead ofonas shown in the readme file
Hint: If the auto-import feature of your IDE does not work, you can add this import by hand:
import { immerOn } from 'ngrx-immer/store';