Reason for comparing
When creating a web application, one of the questions to ask is how data should be managed. If the application must be reactive, it’s best to use ReactiveX (or Rx for short) to create streams of data. The next question is how this could work performant and reliable.
The current trend is to use a Redux-based storage solution, which consists of a Store, Selectors to get data from the store in the form of Observables and Actions to modify the store. This allows for a single source of truth, a read-only state and the flow of data going in one direction. There are a couple of different solutions for Angular. NGRX is by far the most popular, leaving the new kids in town, NGXS and Akita, far behind in popularity.
It is, however, not always needed to have a storage framework solution. Very small applications are easy to create with plain RxJS, if you are quite skilled. In this post, I’ve stacked each of these solutions against each other to see what can be learned.
Table of content
The fighting ring
To compare the four competitors, I’ve set up a simple To Do-application (GitHub) with Angular CLI.
The master branch holds a base of the application, which cannot run on its own. It needs a state management solution from one of the other branches. To make the comparison easier, the base application is written in such a way that each solution only adds files to a statemanagement folder and loads a service and zero or more modules into the AppModule. No other files (except package.json, package-lock.json and logo.png) are to be changed. From an end-user perspective, the application would appear and behave the exact same, no matter which state management solution is used. The logo is added to be able to differentiate which solution is running.
A To Do-application is perfect to demonstrate CRUD. A FakeBackendService is provided to simulate a RESTful API backend. The idea is to load the list only once in the application’s lifetime and then update the state, without needing to fetch everything from the backend again. As such, the FakeBackendService logs its calls to the console for monitoring.
The competitors
NGRX
(v6.1.x) Docs
RxJS powered state management for Angular applications, inspired by Redux
@ngrx/store is a controlled state container designed to help write performant, consistent applications on top of Angular.
NGXS
(v3.2.x) Docs
NGXS is modeled after the CQRS pattern popularly implemented in libraries like Redux and NGRX but reduces boilerplate by using modern TypeScript features such as classes and decorators.
Akita
(v1.7.x) Docs
Akita is a state management pattern, built on top of RxJS, which takes the idea of multiple data stores from Flux and the immutable updates from Redux, along with the concept of streaming data, to create the Observable Data Stores model.
Akita encourages simplicity. It saves you the hassle of creating boilerplate code and offers powerful tools with a moderate learning curve, suitable for both experienced and inexperienced developers alike.
Plain RxJS
(v6.0.x) Docs
One of the aims of a (progressive) web application is to minimize loading time by reducing the package size. In that light, some developers opt to not use a framework, but instead use plain RxJS. To simulate a store as much as possible, I’ve used BehaviorSubjects to hold the state and pipeable operators to modify the state.
Fight
1. Available tooling
Since this post is aimed at developers, it might be best to first evaluate the tools available for developers.
A Redux Devtools plugin exists for Chrome and Firefox, or it can be run as a standalone application. It allows developers to see the impact of a Redux action and time travel between these actions. Another useful feature available to Angular developers is Angular Schematics, which allow to create pieces of code through Angular CLI. None of the solutions have these tools in their default packages and they need to be installed separately.
Redux DevTools
DevTools in NGRX
NGRX provides @ngrx/store-devtools for DevTools. It works as expected, displaying the latest actions with their impact and the resulting state of the store. It’s possible to jump to specific actions and even skip actions. It also allows devs to dispatch an action directly from the DevTools itself, but does not verify that action’s payload. Implementing the tools is as easy as importing the following line to the AppModule:
StoreDevToolsModule.instrument()
NGRX DevTools provide options for displaying a maximum age of actions, displaying a name, logging only to console, sanitizing state and actions and serializing the state.
DevTools in NGXS
Although NGXS is also modeled after CQRS, it behaves a bit differently. It provides @ngxs/devtools-plugin for DevTools. It does, however, not support all functionalities. The latest actions can be viewed with their impact and resulting state. But while it’s possible to jump to specific actions, it’s not possible to skip actions or dispatch new ones using the DevTools. Implementing the tools is just as easy as with NGRX, importing the following line to the AppModule:
NgxsReduxDevtoolsPluginModule.forRoot()
NGXS also provides some options for displaying a maximum age of actions, displaying a name and sanitizing state and actions.
DevTools in Akita
Akita is the only solution not powered by a Redux-like pattern.
That is why it also has limited functionality in the DevTools.
The DevTools are available through @datorama/akita-ngdevtools.
Similar to NGXS, the latest actions can be viewed with their impact and the resulting state.
And similar to NGXS, it’s possible to jump to specific actions in the timeline, but impossible to skip actions or dispatch new ones using the DevTools.
What’s more is that the raw action does not present the actual payload.
When adding custom actions, you also have to name them with the @action
decorator.
Implementing the tools is, as ever, possiblie by importing the following line to the AppModule:
AkitaNgDevtools.forRoot()
Akita’s DevTools plugin also provides some options for displaying a maximum age of actions, a blacklist and a whitelist.
DevTools in plain RxJS
Since RxJS itself is no Redux-based storage solution, it obviously does not provide any support for Redux DevTools at all.
Schematics
Schematics in NGRX
NGRX has quite a lot of schematics available through @ngrx/schematics. It allows to create stores, feature stores, reducers, actions, container components, effects, entity stores all with a lot of options. In my To Do-applications, most of the work was done using two simple commands:
ng g @ngrx/schematics:store AppState --module app.module.ts --root --statePath statemanagement
ng g @ngrx/schematics:entity statemanagement/TodoItem --reducers index.ts
The first command added the StoreModule and StoreDevToolsModule into AppModule and created a reducer in statemanegement/index.ts. The latter command created the following files:
- statemanagement/todo-item.actions.ts, with a lot of premade actions for inserting, updating, upserting, removing one or multiple entities.
- statemanagement/todo-item.model.ts, with a premade model interface I changed to just export the TodoItem interface which I created for the base application.
- statemanagement/todo-item.reducer.ts, and its corresponding spec file handling the generated actions (the spec file did however only test ‘unknown action’) and providing several basic selectors, though I had to modify the following code for the selectors to work:
export const selectTodoItemState = createFeatureSelector<State>('todoItem');
export const {
selectIds,
selectEntities,
selectAll,
selectTotal,
} = adapter.getSelectors(selectTodoItemState);
The attempt to create extra actions using the following command was not as easy as it seemed.
ng g @ngrx/schematics:action statemanagement/Filter
Only one action was created called LoadFilters, with the type [Filter] Load Filters
.
It would’ve been easier if one could specify the name of the action some more.
I ended up extending the generated Entity store with the filter and sorting properties for which I had to add extra reducers and selectors manually. These properties could have been part of a separate state, but I opted to keep the store as simple as possible.
Schematics in NGXS
NGXS does not offer any schematics extensions. It does, however, offer a CLI through ngxs-cli. Although the documentation makes mention of the package @ngxs/ngxs-cli, this package was at the time of writing not available.
Using the CLI, a state file todo-items.state is created, along with todo-items.actions.ts with 1 example action (add), to add an item to an array. Other than that, everything must be done by yourself, including adding importing the module into AppModule. The CLI offers some options for a name, whether or not to create a spec-file, the path and the name of the folder to create.
Schematics in Akita
Akita does offer schematics through akita-schematics. It allows to separately create a store, model, query and service (meant for http), or everything together in what they call a feature. I used the following command to create a feature store:
ng g akita-schematics:feature statemanagement/todoItems
This created the following files:
- statemanagement/state/todo-item.query.ts, which is used for selecting items from the state.
- statemanagement/state/todo-item.model.ts, with a premade model interface I changed to just export the TodoItem interface which I created for the base application.
- statemanagement/state/todo-item.store.ts, which contains the actions that can be performed.
Since I didn’t use the --plain
option, the state created by that command was an extension of EntityState, and already has some actions available for setting, inserting, updating and removing entities.
Similar to NGRX, it was easy to extend the state with a filter and a sort property.
Akita also provides a CLI tool, but I didn’t test this.
Schematics in plain RxJS
Since RxJS is based on operator functions, it’s nearly impossible to have useful schematics for this use case. This means that a developer must write everything by hand.
Tooling summary
Tooling | Redux DevTools | Schematics |
---|---|---|
NGRX | Yes | Yes |
NGXS | Yes, limited | No, but limited CLI |
Akita | Yes, limited | Yes, also CLI |
Plain RxJS | No | No |
2. Features
This is a tough one. I didn’t have use cases in a To Do-application to research every possible feature. But, nevertheless, let’s cover the most useful features and their solutions.
Feature: Asynchronous actions
What is meant with asynchronous actions, is that an action is dispatched to the store and the store is updated in an asynchronous way. An example of this is the use of a FetchItems action, which performs a request to the backend and dispatches one or multiple different actions when that request completes. This is especially useful when using a realtime database or Google Cloud Firestore, which opens a socket and can emit multiple events. In the example application, I’ve implemented this for a one-time fetch of items where possible.
NGRX can handle this with @ngrx/effects, a separate package to be installed. Effects can be added to the root module or to a feature module for lazy loading. They can react on any Observable (not only emitted actions) and must emit a new action. If multiple actions should be emitted, these must be flatMapped. There is also a schematics extension available to generate an Effects class.
NGXS allows actions to be handled asynchronously out-of-the-box. These actions can dispatch different actions, but can also modify the state directly. An Observable or Promise must be returned to notify the dispatcher that the action has been completed.
Akita does not have support for asynchronous actions. The subscription to an asynchronous stream of data must be handled by yourself.
While RxJS is effectively the reason asynchronous actions can exist in Angular, it is quite difficult for novices to update the store from a stream.
Feature: Memoized Selectors
NGRX offers support for Selectors as constants. These can be easily chained in other selectors, making them ideal when the store is being refactored.
NGXS works similar, but uses functions inside the State class. They can be chained, but it’s not as clear to understand as the NGRX solution. A neat feature within NGXS, is the so-called shared selector, which allows to create a selector that can be used with different states.
Akita takes a different approach. A Query class is created, in which functions and constants can be defined. These return Observables, which can be used to obtain a part of the store and can be combined using RxJS operators. Unlike NGRX and NGXS, Akita does not easily offer selecting queries across different states in the store, without creating substates.
As ever, RxJS, must throw in the towel for this. When you need something from the store, you’ll need to use some operators to get that specific item.
Feature: Persistence
NGRX does not offer any persistence logic itself. There is however a 3rd party package available, ngrx-store-localstorage, which works through a meta reducer. It offers some options, one of which is setting the Storage interface and which keys to sync and how to (de)serialize those items.
NGXS does have its official plugin, @ngxs/storage-plugin, a separate module that can be imported into the AppModule. It also has options for setting the Storage interface and which keys to sync and how to (de)serialize those items, but also offers migration strategies. This allows for a version with a radically changed store to not meet with synchronization errors.
Akita’s main package includes a persistState() function. Including this function in the main.ts file allows the state to be stored in either localStorage or sessionStorage. Other options include, setting the key by which the state is saved, and including/excluding several aspects of the store and how to (de)serialize those items.
When using plain RxJS, you’re on your own again.
Other features
The frameworks offer even more features. I’m not going into detail for each of them. Most of them are included in the following summary.
Features summary
Features | NGRX | NGXS | Akita | Plain RxJS |
---|---|---|---|---|
Async actions | Yes, through effects | Yes | No | No |
(Memoized) selectors | Yes | Yes | Yes, as queries | No |
Cross-state selectors | Yes | Yes | No | No |
Offline persistence | 3rd party package | 1st party package | Main package | No |
Snapshot selection without first() | No | Yes | Yes | No |
Forms synchronization | 3rd party packages | 1st party package | Main package | No |
Router synchronization | 1st party package | 1st party package | No | No |
WebSocket | 3rd party package | 1st party package | No | No |
Angular ErrorHandler | No | Yes | No | No |
Meta Reducers | Yes | Yes | No | No |
Lazy loading | Yes | Yes | Yes | No |
Cancellation | No | Yes | No | No |
Side effects | Yes | Yes | No | No |
Web workers | No | No | Yes | No |
Transactions | No | No | Yes | No |
3. Boilerplate code
In this round the boilerplate code is evaluated. This is code that is needed for each part of the state, but differs only a little per state. I opted not to use immer for immutable state changes to give each competitor the same chances.
Also within this section, there is the amount of files needed or generated for the To Do-application.
Starting with NGRX, which generated 9 files through schematics. These files include the reducer file. Even though I created an Entity store, to ease the use of an entity collection, the reducer file still contained a lot of code through the adapter. A lot of which were the reducer cases for all the adapter’s actions, like set, insert, upsert and delete one or many items. Even though these cases only call the adapter’s functions, most of these methods won’t change and it would be nicer if these could have been part of the @ngrx/entity package, like the generated selectors. The same argument holds for the actions created in todo-item.actions.ts
NGXS fairs a little better in this aspect. It generated only 3 files, though each action had to be written out myself and I created extra files for other actions. And even though the action functions can refer to Generic functions, it’s a shame an Entity State with specialized functions is not included in the package.
Akita generated just 4 files using the schematics. Because I used an EntityState, a lot of Query functions and Actions were readily available, without them taking extra space.
With RxJS I managed to create an operator function for each ‘action’ very simply. The application is quite simple, but the method is scalable enough without much overhead.
Summary
Boilerplate | Files generated | Total files (*) | Boilerplate code |
---|---|---|---|
NGRX | 9 | 12 | Heavy |
NGXS | 3 | 7 | Medium |
Akita | 4 | 6 | Low |
Plain RxJS | 0 | 6 | Medium |
4. Community
Based on Google Trends of the past 12 months, NGRX is obviously the most searched for state manager. The reason is likely that it was the first Redux implementation available for Angular.
When looking at the GitHub repositories, NGRX has the most stars (at over 3.5K), followed by NGXS (at 1.4K) and Akita (at around 480). Again this indicates NGRX is the most popular framework.
But what about contributors? Looking at the repositories’ insights, it’s clear that the same sequence is followed. NGRX takes the lead, NGXS a solid second and Akita last. There it’s also visible that NGRX is still under very active development, looking at the commits. NGXS meanwhile stagnated and Akita has a steady pace.
Community summary
feat. | Google Trends | GitHub stars | Contributors | Commits |
---|---|---|---|---|
NGRX | 1st | 1st | 1st | 1st |
NGXS | 2nd | 2nd | 2nd | 3rd |
Akita | 3rd | 3rd | 3rd | 2nd |
5. Dependencies and size
State management does not come out-of-the-box with Angular. There is a need to install extra dependencies. Luckily all these dependencies are available through npm. To make the different implementations as feature-equal as possible, I’ve decided to create entity stores where possible and include dev-tools if available.
Furthermore, all implementations were built with and without production mode. For comparison purposes, the base application measured in at 14.6MB without production mode, and a mere 754KB with production mode.
NGRX is a heavy hitter. It included multiple dependencies for different features. @ngrx/store is the basis. @ngrx/store-devtools, @ngrx/entity, @ngrx/effects and @ngrx/schematics complement this, although the schematics are dev-only. All this gives the packages a weight of 14.9MB without production mode and 786KB with production mode.
NGXS fairs a little better. It only includes @ngxs/store and @ngxs/devtools-plugin. This makes the packages weigh in at 14.8MB without production mode and 778KB with production mode.
Akita also has an all-in-one package for the store. @datorama/akita holds all functionality, while @datorama/akita-ngdevtools and akita-schematics provide some development tools. Despite this, Akita overthrows NGRX with 15.4MB without production mode and matches NGXS with 778KB with production mode. The difference between NGXS and Akita in production mode was a mere 24B.
RxJS is the clear winner here. It needs no extra dependencies whatsoever as RxJS already is a dependency of Angular, making the packages 14.6MB without production mode and 762KB with production mode.
Dependencies and size summary
Size | Non-production (MB) | Production (KB) |
---|---|---|
Base | 14.6 | 754 |
NGRX | 14.9 | 786 |
NGXS | 14.8 | 778 |
Akita | 15.4 | 778 |
Plain RxJS | 14.6 | 762 |
Final score
It’s not easy to just say which solution is the all-time champion. Each of the competitors has its advantages and disadvantages. These are the podium places for each round:
Round | NGRX | NGXS | Akita | Plain RxJS |
---|---|---|---|---|
Tooling | 1st | 3rd | 2nd | |
Features | 2nd | 1st | 3rd | |
Boilerplate code | 3rd | 2nd | 1st | 2nd |
Community | 1st | 2nd | 3rd | |
Dependencies and size | 3rd | 2nd | 2nd | 1st |