19
MiniRx Feature Store vs. NgRx Component Store vs. Akita
MiniRx "Feature Stores" offer simple yet powerful state management.
How does MiniRx Feature Store compare to @ngrx/component-store and @datorama/akita? 10 rounds in the fighting ring will bring clarity!
Disclaimer: I am the maintainer of MiniRx Store, I try to be fair, but it can be difficult from time to time.
To be clear: Component Store and Akita are great state management libraries. It will be an intense fight, but I will make sure that nobody gets hurt!
MiniRx is a full-blown Redux Store powered by RxJS: It includes actions, reducers, meta reducers, memoized selectors, effects and Redux DevTools support.
The Redux pattern is great to manage state at large scale, but it forces us to write boilerplate code (actions, reducers, dispatch actions). This can be overkill for simple features in your application.
For that reason, MiniRx Feature Store offers a more simple form of state management: we can bypass Redux boilerplate and interact directly with a corresponding feature state with the FeatureStore
API:
-
setState()
update the feature state -
select()
select state from the feature state object as RxJS Observable -
effect()
run side effects like API calls and update feature state -
undo()
easily undo setState actions (requires the UndoExtension) -
get state()
imperatively get the current feature state
MiniRx scales nicely with your state management requirements:
- Make hard things simple with the Redux
Store
API - Keep simple things simple with the
FeatureStore
API
In most cases you can default to the FeatureStore
API and fall back to the Redux Store
API to implement the really complex features in your application.
Feature Store uses Redux under the hood:
Behind the scenes a Feature Store is creating a feature reducer and a corresponding setState action.
The feature reducer is registered in the Redux Store and the Feature Store state becomes part of the global state object.
When calling setState()
the Feature Store dispatches its setState action (with the new state as action payload) and the feature reducer will update the feature state accordingly.
See the FeatureStore
source here.
- š¤ Learn more about MiniRx on the docs site
- ā MiniRx on GitHub
- š See it in action in the Angular Demo
- š¤ Feature Store docs
- š MiniRx Basic Tutorial on StackBlitz: See how the Redux API and Feature Store API both add to the global state object
Let's shed some light on MiniRx Feature Store by sending it to the fighting ring together with two other popular state management libraries: @ngrx/component-store and @datorama/akita.
Component Store is a library that helps to manage local/component state. It can be used as an alternative to the "Service with a Subject" approach.
It is build on top of RxJS/ReplaySubject (see here). Services which extend ComponentStore
expose state as RxJS Observables (using the select
method). With the methods setState
and patchState
the state can be updated.
Akita describes itself as a "state management pattern":
It offers a set of specialized classes like Store
, Query
, EntityStore
and more.
Akita Store
is build on top of RxJS/BehaviorSubject (see here).
By using the Akita classes we can build a reactive state service which exposes state as RxJS Observables (using select
on a Query
instance). The update
method of Store
is used to update the state.
MiniRx itself is a "hybrid" Store. It uses Redux and RxJS/BehaviorSubject (see here) under the hood and exposes the powerful Redux Store
API (which is very similar to @ngrx/store and @ngrx/effects).
At the same time MiniRx allows you to bypass the infamous Redux boilerplate with the FeatureStore
API.
You can create a reactive state service by extending FeatureStore
.
RxJS Observables (returned by the select
method) inform about state changes and the state can be changed by calling setState
.
Mhhh..., this sounds all very similar, but where are the differences then? It's time to prepare the fighting ring! :)
10 rounds to go!
What does the basic setup of a reactive state service look like?
All setups share the same ingredients: A state interface and initial state.
FYI: The state interface must be object-like: you can not state-manage just a plain number
or string
.
interface CounterState {
count: number;
}
const initialState: CounterState = {
count: 42
}
The state service extends FeatureStore
:
@Injectable({providedIn: 'root'})
export class CounterStateService extends FeatureStore<CounterState> {
count$: Observable<number> = this.select(state => state.count);
constructor() {
super('counter', initialState)
}
increment() {
this.setState(state => ({count: state.count + 1}))
}
decrement() {
this.setState(state => ({count: state.count - 1}))
}
}
MiniRx Feature Store has to provide the initial state and a feature key: "counter".
The key is used to register the "counter" state in the global state object.
With Component Store we extend ComponentStore
and provide an initial state:
@Injectable({providedIn: 'root'})
export class CounterStateService extends ComponentStore<CounterState> {
count$: Observable<number> = this.select(state => state.count);
constructor() {
super(initialState)
}
increment() {
this.setState(state => ({count: state.count + 1}))
}
decrement() {
this.setState(state => ({count: state.count - 1}))
}
}
The Component Store setup looks very similar to Feature Store, however the feature key is not needed because every ComponentStore
instance lives independently.
FYI: The Component Store initial state parameter is optional (see docs here).
With Akita, we create two services: One extends Store
and the other one extends Query
:
@Injectable({providedIn: 'root'})
@StoreConfig({ name: 'counter' })
export class CounterStateService extends Store<CounterState> {
constructor() {
super(initialState)
}
increment() {
this.update(state => ({count: state.count + 1}))
}
decrement() {
this.update(state => ({count: state.count - 1}))
}
}
@Injectable({providedIn: 'root'})
export class CounterQuery extends Query<CounterState> {
count$: Observable<number> = this.select(state => state.count);
constructor(store: CounterStateService) {
super(store);
}
}
The Akita setup is the most boilerplaty. Extending Store
is similar to the other setups. A feature key is provided via the @StoreConfig
decorator.
To access the state you have to extend Query
and provide the Store
instance.
Also, the components have to talk to both the Query
and the Store
instance in order to read and write state.
Regarding the basic setup..., let's look at the corresponding bundle sizes (using source-map-explorer).
combined: 152.39 KB
combined: 152.25 KB
combined: 151.61 KB
Akita is the most lightweight, and MiniRx is almost 1 KB bigger.
But keep in mind that MiniRx Feature Store uses Redux under the hood
and the Redux API is always available. Using the MiniRx Redux API will not add much to the total bundle size.
MiniRx Feature Store + Store API (Store + Effects) using Angular Integration (mini-rx-store-ng)
combined: 156.9 KB
combined: 164.17 KB
combined: 171.45 KB
You can review the different setups in this repo and run source-map-explorer yourself: https://github.com/spierala/mini-rx-comparison
How do the different store solutions relate to local (component state) and global state? What is the store lifespan?
MiniRx at its heart is a Redux Store with one global state object ("Single source of truth"). Also, MiniRx Feature Stores register a "slice" of state into the global state object.
The focus of MiniRx is clearly global state which has the lifespan of the application.
But Feature Stores are destroyable... Their state can be removed from the global state object. Therefore, Feature Stores can be used for "Local Component State", which has the lifespan of a component.
See an example in the MiniRx Angular demo.
Component Stores live independently and are not related to something like a global state (e.g. when using @ngrx/store).
The lifespan of a Component Store can be bound to a component ("Local Component State"), but it can also take the lifespan of the application.
The Akita Stores live independently next to each other. There is no real global state. You can use Akita Stores (which are destroyable too) for "Local Component State" following this guide from the Akita docs.
MiniRx can use Redux DevTools with the built-in Redux DevTools Extension.
Every Feature Store state becomes part of the global state object, and it can be inspected with the Redux DevTools.
There is no official solution for Redux DevTools with Component Store.
Akita has a PlugIn for Redux DevTools support.
FYI: The separate Store states are merged into one big state object to make all state inspectable with the Redux DevTools. See the Akita DevTools source here.
How can we select state from other store instances and pull that state into our current store (state service)?
Every Feature Store state integrates into the global state object. Therefore, the corresponding feature states can be selected at anytime from the Redux Store
(!) instance using store.select
.
Alternatively you can use RxJS combination operators like combineLatest
or withLatestFrom
to combine state from other Feature Stores with state Observables of your current Feature Store.
The Component Store select
method also accepts a bunch of Observables to depend on (see docs here).
Of course these Observables can come from other services. Like this it is straight-forward to depend on (observable) state of other ComponentStore
instances.
Akita has combineQueries
to combine state from different Query
instances. combineQueries
is basically RxJS combineLatest
.
See the Akita combineQueries source here.
Memoized selectors can help to improve performance by reducing the number of computations of selected state.
The selectors API (createSelector
) is also great for Composition: Build selectors by combining existing selectors.
Examples for memoized selectors:
MiniRx comes with memoized selectors out-of-the-box.
You can use the same createFeatureSelector
and createSelector
functions for the Redux Store
API and for the FeatureStore
API.
Read more in the Feature Store memoized selectors documentation.
Example code of memoized selectors in the MiniRx Angular Demo: Todos State Service
There is no official solution for Component Store.
You could add @ngrx/store to use the memoized selectors, but it would probably be overkill to add the NgRx Redux Store just for that reason. Redux Reselect could be a better alternative.
No memoized selectors. You could most probably add Redux Reselect.
Effects are used to trigger side effects like API calls.
We can also handle race-conditions more easily within an Effect by using RxJS flattening operators (switchMap
, mergeMap
, etc.).
MiniRx Feature Store has Effects (https://mini-rx.io/docs/effects-for-feature-store).
FYI: Feature Store Effects have their equivalent in the Redux API of MiniRx: https://mini-rx.io/docs/effects
Yes, there are Effects: https://ngrx.io/guide/component-store/effect
Yes, there are Effects: https://datorama.github.io/akita/docs/angular/effects.
Effects come with a separate package (@datorama/akita-ng-effects).
The Effects API is not tied to a Store
instance.
How can we undo state changes?
MiniRx has the UndoExtension to support Undo of state changes.
This is especially helpful if you want to undo optimistic updates (e.g. when an API call fails). Both the FeatureStore
and the Redux Store
API can undo specific state changes.
Feature Store exposes the undo
method.
Read more in the MiniRx docs: Undo a setState Action
No support for undo.
Akita has a State History PlugIn to undo state changes (https://datorama.github.io/akita/docs/plugins/state-history/).
The API is much bigger than the one of Feature Store. But it seems to be difficult to undo a very specific state change (which is important when undoing optimistic updates).
Immutability is key when using state management: We only want to allow explicit state changes using the corresponding API (e.g. by using setState
, update
or by dispatching an Action in Redux).
Mutating state however, might lead to unexpected behavior and bugs.
Immutable state helps to avoid such accidental state changes.
MiniRx offers the Immutable State Extension to enforce immutable data.
When the ImmutableStateExtension
is added to the MiniRx Store both the Redux Store
API and the FeatureStore
API will use immutable data.
The Immutable State Extension "deepfreezes" the global state when state is updated. Mutating state will throw an exception.
There is nothing in Component Store which can enforce immutability.
Akita "deepfreezes" the state object when state is updated (only in DEV mode). See the corresponding source code here: https://github.com/datorama/akita/blob/v6.2.0/libs/akita/src/lib/store.ts#L181
MiniRx is framework-agnostic. You can use MiniRx with any framework or even without framework.
See here the MiniRx Svelte Demo: https://github.com/spierala/mini-rx-svelte-demo
Component Store is tied to Angular. Angular is a peer dependency in the package.json.
Akita is also framework-agnostic. You can see in this article how Svelte and Akita play together: Supercharge Your Svelte State Management with Akita
Yes, you made it! I hope that you had fun to watch this fight!
All competitors showed their skills, none of them went to the ground!
Who was your favorite?
Give it a star on GitHub:
- ā MiniRx on GitHub
- ā NgRx on GitHub
- ā Akita on GitHub
For completeness, I want to list a few things that were out of scope for this fight:
- Akita: EntityStore, Transactions, Akita Immer, Persist State, CLI
- Component Store:
updater
method,tapResponse
operator
Another cool lib which goes into the same direction as NgRx Component Store:
https://github.com/rx-angular/rx-angular/blob/master/libs/state/README.md
Maybe see you in the next fight! :)
- There was once a legendary fight in 2018, organized by Orjan de Smet: NGRX VS. NGXS VS. AKITA VS. RXJS: FIGHT!
- Photo by Attentie Attentie on Unsplash
- Photo by Dan Burton on Unsplash
- Photo by Matthew Payne on Unsplash
19