Messaging based state management is a staple of enterprise-level applications. Not pursuing a Flux/Redux type of state management solution in a large application often results in unwanted side effects as there is no single source of truth. If you come from a React/Redux background, you may not be sure which popular Redux Angular implementation to go with for your next Angular enterprise application.
Why use Redux/Flux in enterprise applications?
A benefit to Redux is the ability to model complex data flow relationships between entities in the front end once and enjoying this predictable behavior in your app. You can then use tools like Redux DevTools to see exactly how data is flowing through your application, and even go back in time to see how the state of the UI changes over time.
Furthermore, if you use Redux throughout your application, you can update Angular’s change detection strategy to OnPush.
This drastically improves performance by only running change detection when a stateful variable used in a component is reassigned in memory and is trivial to get working when your state management library is implemented correctly.
So which Angular Redux library should you use for your next project? In this article, I’ll be reviewing the most popular state management solutions in Angular and helping you decide which is best for your project. Currently leading the pack are NGRX, NGXS, and Nikita, which all use RXJS behind the scenes. You can avoid a state management library altogether by using RXJS directly. However, this is more appropriate for smaller projects, so discussion of using RXJS directly will not be discussed
NGRX
First, we will describe NGRX. It is the most popular state management library in Angular by far with 5,900 stars on Github. They have made strides to significantly reduce their boilerplate code as much as possible, making it a more desirable option in 2020. So we can see this in action, let’s create a simple store in NGRX.
First let’s create our actions:
import { createAction } from '@ngrx/store'; export const feedAnimals = createAction('[Zoo] Feed');
And the accompanying reducer:
import { createReducer, on } from '@ngrx/store'; import { feedAnimals } from './zoo.actions'; export interface State { name: string; feed: boolean; } export const initialState: State = { name: 'zoo', feed: false }; const _zooReducer = createReducer(initialState, on(feedAnimals, state => {...state, feed: !state.feed}) ); export function zooReducer(state, action) { return _zooReducer(state, action); }
NGRX has migrated from using a rather bloated switch statement to a much more concise and clean style of using the on method from @ngrx/store.
And finally, the consuming component:
import { Component } from '@angular/core'; import { Store, select } from '@ngrx/store'; import { Observable } from 'rxjs'; import { ZooState } from '../zoo.state'; import { feedAnimals } from '../zoo.actions'; @Component({ ... }) export class ZooComponent { feed$: Observable<boolean>; constructor(private store: Store<ZooState>) { this.feed$ = store.pipe(select('feed')); } feedAnimals() { this.store.dispatch(feedAnimals()); } }
In the above snippet, you’re injecting the store service to dispatch actions and retrieve the current state. Be aware that if you need asynchronous data (e.g, making an HTTP call) you will have to create a separate file called an effect, which is a service that dispatches actions based on the results of asynchronous calls.
As far as tooling, NGRX has full functionality in Redux DevTools, making it a dependable option.
NGXS
Next is NGXS. So why NGXS? NGXS has less of a dependency on knowledge of RXJS, combines actions and reducers into one source file, and is more concise than NGRX.
Since reducers are not a concept found in NGRX, how do you mutate the store? NGRX actions mutate the store directly, saving you the mental capital of managing one more file for each store in your app.
Let’s take a look at NGXS’s merging of reducers and actions:
import { Injectable } from '@angular/core'; import { State, Action, StateContext } from '@ngxs/store'; // Actions export class FeedAnimals { static readonly type = '[Zoo] FeedAnimals'; } export interface ZooStateModel { name: 'string'; feed: boolean; } @State<ZooStateModel>({ name: 'zoo', defaults: { feed: false } }) @Injectable() export class ZooState { @Action(FeedAnimals) feedAnimals(ctx: StateContext<ZooStateModel>) { const state = ctx.getState(); ctx.setState({ ...state, feed: !state.feed }); } }
Everything is essentially contained in this one file. Now let’s see how a component would consume ZooState:
import { Store, Select } from '@ngxs/store'; import { Observable } from 'rxjs'; import { FeedAnimals } from './animal.actions'; @Component({ ... }) export class ZooComponent { @Select(state => state.feed) feed$: Observable<boolean>; constructor(private store: Store) {} feedAnimal(name: string) { this.store.dispatch(new FeedAnimals()); } }
Instead of wiring up your connections to the state in the constructor, you can use the @Select selector. This cuts this boilerplate in half.
Akita
Akita’s angle is attracting developers that are more agreeable to object-oriented design principles as opposed to the functional programming style of the preceding options. Akita is deployed in production by its creator Datorama, giving them a degree of accountability in creating a library suitable for production applications. The other benefit to Akita is its focus on eliminating boilerplate, which is the most common developer grievance with NGRX. It is also not exclusive to Angular as it can be used with React, VueJS, etc.
Let’s create our ZooStore in Akita.
import { Store, StoreConfig } from '@datorama/akita'; export interface ZooState { name: string; feed: boolean; } export function createInitialState(): SessionState { return { feed: false, name: 'zoo' }; } @StoreConfig({ name: 'zoo' }) export class ZooStore extends Store<ZooState> { constructor() { super(createInitialState()); } }
Next let’s take a look at Akita’s version of Redux “actions”, which are described as updates in Akita-speak.
import { ZooStore } from './zoo.store'; export class ZooService { constructor(private zooStore: ZooStore) {} updateFeed() { this.zooStore.update(state => ({ feed: !state.feed })); } }
While not as concise as NGXS, the argument can be made that this is the most readable implementation of the Redux design pattern. It is also reminiscent of traditional Angular services, making the app more approachable to both veterans and newcomers to Redux-type state management.
Next look at a query with Akita, which is their version of selectors:
import { Query } from '@datorama/akita'; export class ZooQuery extends Query<ZooState> { // Two methods of retrieving from the state selectFeed$ = this.select(state => state.feed); selectName$ = this.select('name'); // Multi selectors multi$ = this.select(['name', 'feed']) // { name, feed } multi$ = this.select([state => state.name, state => state.feed]) // [name, feed] constructor(protected store: ZooStore) { super(store); } }
The above snippet from the Akita docs perfectly encapsulates its OOP workflow. You’re extending classes and using the this context keyword as you would in traditional OOP applications as opposed to importing every keyword. As noted above, query in Akira is similar to a selector in NGRX and NGXS but with substantially less code and imports at no cost to code readability.
Finally, let’s take a look at how you would consume those queries in a component:
@Component({ ... }) export class ZooComponent implements OnInit { feed$: Observable<boolean>; constructor(private zooQuery: ZooQuery, private zooService: ZooService) {} ngOnInit() { this.feed$ = this.zooQuery.selectFeed$; } updateFeed() { this.zooService.updateFeed(); } }
As far as tooling, Akita is compatible with Redux DevTools and, as of version 1.9, supports executing actions from the DevTools console. There may be some bugs here, however, as there are some related bugs reported in Akita’s issues feed.
Conclusion
NGRX is the most faithful implementation of Redux in Angular. It is by far the most verbose and most used by Angular developers. That being said, verbosity may be unwanted in a very large application where managing package size is a concern. NGXS has the edge here but Akita also removes the separation of actions and reducers with no sacrifice to readability.
My objective was to be as impartial as possible in outlining the options for state management in Angular but, after implementing state management in Angular over the years, these are my thoughts:
NGXS is a reduction in boilerplate from NGRX that is less significant now than a year ago. Why? NGRX have improved their library with better decorators and more concise methods. Where I would have leaned more heavily toward NGXS in the past to avoid excessive boilerplate code, I would now go with NGRX because of its longevity and better community support.
Akita is exciting in that it is an object-oriented departure from what we have seen in the past, yet it is still recognizably Redux. Although it has seen significant adoption it is still behind even NGXS in that downloads – but, with full Redux DevTools and Angular Ivy support, it’s an option worth considering for future projects.