Computed Values with RxJS and @ngrx/store
July 25, 2016
Cross-post from the Rangle.io blog.
Introduction
@ngrx/store is a popular store architecture for Angular 2. It promotes one-way data binding for components:
- Components subscribe to updates from the store
- Components dispatch events to the store
- Reducers receive dispatched events and modify the store data structure
- …and repeat.
We’ve used @ngrx/store on several Angular 2 projects at Rangle. @ngrx/store was built specifically for Angular 2 and leverages the wonderful JavaScript Reactive Extensions library (RxJS). This allows updates to the store to be consumed as observable streams in our components. We’ve previously written how Observables are central to Angular 2.
@ngrx/store in principle is very similar to Redux and the concepts should be familiar to someone with knowledge of Redux. Most Redux best practices have an equivalent in @ngrx/store and we’ll be covering one of those today: computed values.
Reselect with Redux
First let’s contrast with the best practices in Redux. Reselect is a selector library for Redux. Selectors are memoized functions used to compute derived values from the store. Being memoized implies values are only computed when the inputs to the selector function change. With Reselect, we don’t need to put computed values in the store. We can instead store the minimal state and recompute derived data only when necessary. We’ve covered this topic recently in an article titled Improving React and Redux performance with Reselect.
We can achieve a similar effect with @ngrx/store using built-in RxJS’s methods. I’ll illustrate on an example application.
Example Application
The examples below use the following monotonically increasing counter store instrumented into an Angular 2 TypeScript application:
counter.reducer.ts
import {ActionReducer, Action} from '@ngrx/store';
export const counter: ActionReducer<number> = (state: number = 0, action: Action) => {
switch(action.type) {
case 'INCREMENT':
return state + 1;
default:
return state;
}
};
bootstrap.ts
import {bootstrap} from '@angular/platform-browser-dynamic';
import {App} from './app';
import {provideStore} from '@ngrx/store';
import {counter} from './reducers/counter';
bootstrap(App, [provideStore({counter})]);
Note: The examples in this blog post can be found on Plunker.
Computed values with RxJS
In the examples below we’ll be select
ing a value from the store and performing an operation on it. We need the store instance to do this, which can be added to a component through Angular 2‘s dependency injection. The instance of the store is technically a BehaviorSubject which inherits the methods from Observable.
select
shares the same performance characteristics as Reselect, as it uses the distinctUntilChanged
operator on the observable it returns (see here). This effectively memoizes any subscribers to select
, similar to Reselect.
Map
For computing a value from the most recent store data, we can combine @ngrx/store’s select
method with RxJS’s map
operator. map
will receive the latest value from the store and apply a transformation to it, similar to JavaScript’s map
. In my experience map
is the most common operation used when computing values from the store.
import {Component} from '@angular/core';
import {Store} from '@ngrx/store';
import {Observable} from 'rxjs/Observable';
import {IStore} from './store.interface';
import 'rxjs/Rx';
@Component({
selector: 'power',
template: `
<div>
<h2>Power (map example)</h2>
<ul>
<li>2<sup></sup> = </li>
</ul>
</div>
`
})
export class PowerComponent {
counter$: Observable<number>;
power$: Observable<number>;
constructor(private store: Store<IStore>) {
this.counter$ = this.store.select('counter');
this.power$ = this.counter$.map((value) => Math.pow(2, value));
}
}
Scan
RxJS’s scan
operator is similar to reduce
in JavaScript, except reduce
acts on arrays while scan
acts on streams. Streams differ from JavaScript arrays in that values are added to them over time. They are not operated on entirely at once. scan
allows us to operate on all values of a stream over time and return a singular value. This value is updated when new values are added to the store. Here we’ll use scan
to compute the factorial of the counter in the store:
import {Component} from '@angular/core';
import {Store} from '@ngrx/store';
import {Observable} from 'rxjs/Observable';
import {IStore} from './store.interface';
import 'rxjs/Rx';
@Component({
selector: 'factorial',
template: `
<div>
<h2>Factorial (scan example)</h2>
<ul>
<li>! = </li>
</ul>
</div>
`
})
export class FactorialComponent {
counter$: Observable<number>;
factorial$: Observable<number>;
constructor(private store: Store<IStore>) {
this.counter$ = this.store.select('counter');
this.factorial$ = this.counter$.scan((acc, value) => acc * value);
}
}
Selecting across keys
You’ll notice in our previous examples computed values were based on one key from our store (through select('keyName')
). What if we want a value derived from multiple store keys?
We can pass @ngrx/store’s select
a function, allowing us to select arbitrary subsets of store data instead of a single key. For example, if our store also had a name
key and we wanted to grab them both, we could do the following:
this.counterAndNameSelect$ = this.store.select((state) => {
return {
counter: state.counter,
name: state.name
};
});
Additionally, RxJS allows us to combine multiple streams into one by using the combineLatest operator. This can be used as alternative to passing select
a function. This is equivalent to the select
above:
this.counterAndNameCombine$ = this.store.select('counter')
.combineLatest(this.store.select('name'), (counter, name) => {
return {
counter,
name
};
});
Use with Angular 2
Angular 2 treats RxJS streams as first class citizens. Observable’s can be leveraged directly in HTML templates by using Angular’s async
pipe. In our components, we can save Observables as public
values and access them in our templates without having to manually subscribe
. This is the approach we followed in the above examples.
Resources
@ngrx/store puts the full power of RxJS is as your disposal! I’ve only shown as few ways this power can be harnessed. For more information on @ngrx, Redux and RxJS, see the following resources:
- Improving React and Redux performance with Reselect
- Observables and Reactive Programming in Angular 2
- Comprehensive Introduction to @ngrx/store
- Learn RxJS
- Computing Derived Data with Reselect
- The introduction to Reactive Programming you’ve been missing
- Full list of RxJS Observable operators
Thanks to the support squad at Rangle.io, namely Evan Schultz and Cosmin Ronnin for reviewing this.