This article shows how you could setup Redux in an Angular application using ngrx. Redux provides a really great way of managing state in an Angular application. State Management is hard, and usually ends up a mess when you invent it yourself. At present, Angular provides no recommendations or solution for this.
Thanks to Fabian Gosebrink for his help in learning ngrx and Redux. The to Philip Steinebrunner for his feedback.
Code: https://github.com/damienbod/AngularNgrx
History
2021-01-20 Updated to Angular 11.1.0, ngrx stores
2021-01-20 Updated to Angular 11.0.9, API in .NET 5
2019-02-06 Updated to Angular 7.2.4, latest NGRX packages
2019-02-05 Updated to Angular 7.2.3, latest npm, nuget packages
2017-11-05 Updated to Angular 5 and Typescript 2.6.1
The demo app uses an Angular component for displaying countries using the public API https://restcountries.eu/. The view displays regions and the countries per region. The data and the state of the component is implemented using ngrx.
Note: Read the Redux documentation to learn how it works. Here’s a quick summary of the redux store in this application:
- There is just one store per application while you can register additional reducers for your Feature-Modules with StoreModule.forFeature() per module
- The store has a state, actions, effects, and reducers
- The actions define what can be done in the store. Components or effects dispatch these
- effects are use to do API calls, etc and are attached to actions
- reducers are attached to actions and are used to change the state
The following steps explains, what is required to get the state management setup in the Angular application, which uses an Angular service to request the data from the public API.
Step 1: Add the ngrx packages
Add the latest ngrx npm packages to the packages.json file in your project.
"@ngrx/effects": "10.1.2", "@ngrx/entity": "10.1.2", "@ngrx/router-store": "10.1.2", "@ngrx/store": "10.1.2", "@ngrx/store-devtools": "10.1.2",
Step 2: Add the ngrx setup configuration to the app module.
In this app, a single Ngrx store will be used per module. The ngrx configuration needs to be added to the app.module and also each child module as required. The StoreModule, EffectsModule and the StoreDevtoolsModule are added to the imports array of the NgModule.
... import { EffectsModule } from '@ngrx/effects'; import { StoreModule } from '@ngrx/store'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; @NgModule({ imports: [ BrowserModule, AppRoutes, SharedModule, CoreModule.forRoot(), AboutModule, HomeModule, CountryModule, StoreDevtoolsModule.instrument({ maxAge: 25 // Retains last 25 states }), StoreModule.forRoot({}), EffectsModule.forRoot([]) ], declarations: [ AppComponent ], bootstrap: [AppComponent], }) export class AppModule { }
Step 3: Create the interface for the state.
This can be any type of object, array.
import { Country } from '../../models/country'; export interface CountryState { countries: Country[]; loading: boolean; }
Step 4: Create the actions
Create the actions required by the components or the effects. The constructor params must match the params sent from the components or returned from the API calls.
import { Country } from './../../models/country'; import { createAction, props } from '@ngrx/store'; import { Region } from './../../models/region'; export const getAllCountriesAction = createAction('[countries] get countries'); export const getAllCountriesFinishedAction = createAction( '[countries] get countries Finished', props<{ payload: Country[] }>() ); export const getRegionAction = createAction( '[Region] get Region', props<{ payload: Region }>() ); export const getRegionFinishedAction = createAction( '[Region] get Region Finished', props<{ payload: Region }>() ); export const collapseRegionAction = createAction( '[Region] collapse Region', props<{ payload: Region }>() );
Step 5: Create the effects
Create the effects to do the API calls. The effects are mapped to actions and when finished call another action.
import { Injectable } from '@angular/core'; import { Actions, ofType, createEffect } from '@ngrx/effects'; import { of } from 'rxjs'; import { catchError, map, switchMap, } from 'rxjs/operators'; import * as countryAction from './country.action'; import { Country } from './../../models/country'; import { CountryService } from '../../core/services/country.service'; @Injectable() export class CountryEffects { constructor( private countryService: CountryService, private actions$: Actions ) {} getCountries$ = createEffect(() => this.actions$.pipe( ofType(countryAction.getAllCountriesAction), switchMap((action) => this.countryService.getAll().pipe( map((payload: Country[]) => countryAction.getAllCountriesFinishedAction({ payload }) ), catchError((error) => of(error)) ) ) ) ); }
Step 6: Implement the reducers
Implement the reducer to change the state when required. The reducer takes an initial state and executes methods matching the defined actions which were dispatched from the components or the effects.
import { CountryState } from './country.state'; import { Region } from './../../models/region'; import * as countryAction from './country.action'; import { createReducer, on, Action } from '@ngrx/store'; export const initialState: CountryState = { countries: [], loading: false, }; const countryReducerInternal = createReducer( initialState, on( countryAction.collapseRegionAction, countryAction.getAllCountriesAction, countryAction.getAllCountriesFinishedAction, countryAction.getRegionAction, countryAction.getRegionFinishedAction, (state) => ({ ...state, loading: true, }) ), on(countryAction.getAllCountriesFinishedAction, (state, { payload }) => ({ ...state, loading: false, countries: [...payload], })) ); export function countryReducer( state: CountryState | undefined, action: Action ): any { return countryReducerInternal(state, action); }
Step 7: Create the selectors:
import { ActionReducerMap, createFeatureSelector, createSelector, } from '@ngrx/store'; import { Country } from './../../models/country'; import { Region } from './../../models/region'; import { CountryState } from './country.state'; export const worldStoreName = 'world'; export const selectWorldStore = createFeatureSelector(worldStoreName); export const selectLoading = createSelector( selectWorldStore, (state: CountryState) => state.loading ); export const selectCountries = createSelector( selectWorldStore, (state: CountryState) => state.countries ); export const selectRegions = createSelector( selectCountries, (countries: Country[]) => { const allRegions = countries.map((x) => x.region); const allRegionsWithoutDuplicates = [...new Set(allRegions)]; return allRegionsWithoutDuplicates.map((region) => { const allCountriesOfRegion = countries.filter((x) => x.region === region); return { name: region, countries: allCountriesOfRegion, } as Region; }); } );
Step 8: Configure the module.
Important here is how the StoreModule.forFeature is configured. The configuration must match the definitions in the components which use the store.
import { CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CountryComponent } from './components/country.component'; import { CountryRoutes } from './country.routes'; import { EffectsModule } from '@ngrx/effects'; import { StoreModule } from '@ngrx/store'; import { CountryEffects } from './store/country.effects'; import { countryReducer } from './store/country.reducer'; @NgModule({ imports: [ CommonModule, FormsModule, HttpClientModule, CountryRoutes, StoreModule.forFeature('world', countryReducer), EffectsModule.forFeature([CountryEffects]), ], declarations: [CountryComponent], exports: [CountryComponent], }) export class CountryModule {}
Step 8: Create the component
Create the component which uses the store. The constructor configures the store matching the module configuration from the forFeature and the state as required. User actions dispatch events using the actions, which if required calls the an effect function, which then calls an action and then a reducer function which changes the state.
import { Component, OnInit } from '@angular/core'; import { select, Store } from '@ngrx/store'; import { Observable } from 'rxjs'; import { CountryState } from '../store/country.state'; import * as CountryActions from '../store/country.action'; import { Region } from './../../models/region'; import { selectRegions } from '../store/country.selectors'; import { getAllCountriesAction } from '../store/country.action'; @Component({ selector: 'app-country-component', templateUrl: './country.component.html', styleUrls: ['./country.component.scss'], }) export class CountryComponent { async: any; // { name: 'Africa', expanded: false, countries: [] }, // { name: 'Americas', expanded: false, countries: [] }, // { name: 'Asia', expanded: false, countries: [] }, // { name: 'Europe', expanded: false, countries: [] }, // { name: 'Oceania', expanded: false, countries: [] } allRegions$: Observable<Region[]>; constructor(private store: Store<any>) { this.allRegions$ = this.store.pipe(select(selectRegions)); this.store.dispatch(getAllCountriesAction()); } toggleExpanded(region: Region): void { region.expanded = !region.expanded; } }
Step 9: Use the state objects in the HTML template.
It is important not to forget to use the async pipe when using the state from ngrx. Now the view is independent from the API calls and when the state is changed, it is automatically updated, or other components which use the same state.
<div class="container-fluid"> <div *ngIf="allRegions$ | async as allRegions"> <div class="row" *ngIf="allRegions?.length <= 0; else content"> <span>No items found</span> </div> <ng-template #content> <div class="row"> <div class="table-responsive"> <table class="table"> <thead> <tr> <th>#</th> <th>Name</th> <th>Population</th> <th>Capital</th> <th>Flag</th> </tr> </thead> <tbody> <ng-container *ngFor="let region of allRegions; let i = index"> <tr> <td class="text-left td-table-region" *ngIf="!region.expanded" > <span (click)="toggleExpanded(region)">►</span> </td> <td class="text-left td-table-region" *ngIf="region.expanded"> <span type="button" (click)="toggleExpanded(region)" >▼</span > </td> <td class="td-table-region">{{ region.name }}</td> <td class="td-table-region"> </td> <td class="td-table-region"> </td> <td class="td-table-region"> </td> </tr> <ng-container *ngIf="region.expanded"> <tr *ngFor="let country of region.countries; let i = index"> <td class="td-table-country"> {{ i + 1 }}</td> <td class="td-table-country">{{ country.name }}</td> <td class="td-table-country">{{ country.population }}</td> <td>{{ country.capital }}</td> <td><img width="100" [src]="country.flag" /></td> </tr> </ng-container> </ng-container> </tbody> </table> </div> </div> </ng-template> </div> </div>
Redux DEV Tools
The redux-devtools chrome extension is really excellent. Add this to Chrome and start the application.
When you start the application, and open it in Chrome, and the Redux state can be viewed, explored changed and tested. This gives you an easy way to view the state and also display what happened inside the application. You can even remove state changes using this tool, too see a different history and change the value of the actual state.
The actual state can be viewed:
Links:
https://egghead.io/courses/getting-started-with-redux
https://github.com/ngrx/platform/blob/master/docs/store-devtools/README.md
https://gist.github.com/btroncone/a6e4347326749f938510#core-concepts
https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en
https://egghead.io/courses/building-a-time-machine-with-angular-2-and-rxjs
Hi Damien
You write “A store exists per module”. Correctly you should say that there is just one store per application while you can register additional reducers for your Feature-Modules with StoreModule.forFeature() per module.
I like it that ngrx currently gets more attention. A lot of people did not know about it or didn`t have used it yet. But its totally worth to be used in a lot of web applications because it makes state management so much easier. I use it in production for over 1 year.
Cheers
Philip
Hi Philip
Thanks for the feedback. I’ll update
Greetings Damien
[…] Getting started with Angular and Redux (Damien Bowden) […]
Thanks for awesome articles!
Just FYI for the AOT builds when using ngrx you can’t use functions inside the decorators. So have to implement a reducer factory and use an InjectionToken if you need to parametrize the factory (like in case if component can be configured to use different reducers depending on some external condition).
Also your webpack configs wasn’t working for us with raw- and style-loaders for css and scss. With that there was an exception stating something about that styles is expected to be an array… Or something along those lines. So had to change raw- and style-loaders to the to-string-loader. That fixed the issue.
Finally, the vendor.ts doesn’t always work to import the stylesheets for some reason. So we created a vendor.scss instead to import the css and scss using @import statement. In this case if you need to import some stylesheets from the node_modules subfolder then you don’t need to specify the full path, but instead you can just prefix with ~ symbol: @import “~font-awesome/”
Thanks for the super feedback, very grateful!
Will have a luck into this.
Greetings Damien
I would like to thank you for this fantastic tutorial. In order to run it in the directory:
AngularRedux/src/AngularRedux I executed:
– npm run build-production or npm run build-dev
– npm start
I wasn’t able to execute the example. May you please give me a suggestion ?
Best Regards,
Francesco
Hi Francesco, thanks. It should work now, updated everything to the latest versions and tested it.
Use npm run build, and start using IISExpress. I have set it up to run on HTTPS.
I have a problem with npm run build-production, will look into this tomorrow
Greetings Damien
npm run build-production working now
Hi Damien, I very appreciated your fix, now it works. Why did you use Redux as library ?
Best Regards,
Francesco
I used NGRX. This is the most popular from the Redux implementations for Angular. I don’t know if it’s the best, but it works good. I use state management for larger Angular applications, I avoid it on simple, or small UIs.