Skip to content

Computed values and selectors

Timo Dörr edited this page Jun 6, 2017 · 8 revisions

Various approaches exist to compute values automatically by deducing from the state (for example, the MobX library or Reselect). As with async operations we won't need an additional library in Reactive-State as RxJS provides us with everything needed to create computed values or dynamic/memoized selectors (observable values are the core of RxJS and already form the basis of Reactive-State).

Computed Values

Deducing values from RxJS Observables is the core functionality provided by RxJS. Combining it with a .select()ed state from Reactive-State, we can easily create new Observables that automatically deduce from the state, and update whenever the state updates:

const store = Store.create({
    integers: [2, 4, 1, 3],
    quoteOfTheDay: "This sentence no verb"
});

const sortedIntegers: Observable<number[]> =
    store.select(state => state.integers)
         .map(ints => ints.sort((a, b) => a - b))
         // emits [1, 2, 3, 4]

const sumOfIntegers: Observable<number> =
    store.select(state => state.integers)
         .switchMap(ints => Observable.from(ints)
                                      .scan((acc, n) => acc + n, 0))
         // emits 10

const uppercaseQuote: Observable<string> =
    store.select(state => state.quoteOfTheDay)
         .map(quote => quote.toUpperCase())
         // emits "THIS SENTENCE NO VERB"  
                            

All of the above example just employ basic RxJS operators. It is important to note, that computed values (which are just observables) do not belong into the state (they are not plain object and not serializable) just as in the Redux philosophy. You would instead pass the computed Observables around (i.e. through importing them, supplying them as props in React or providing them to the DI in Angular2.

In a more complex example, we can use a customer id and map it to a REST response, and then to a business object.

const store = Store.create({
   activeCustomerId: "1234",
   /* ... */
});

const customerModel = 
    store.select(state => state.activeCustomerId)
         .switchMap(id => fetch(`/customers/${id}`))
         .switchMap(response => response.json())
         .map(json => ({ firstName: json.firstName, lastName: json.lastName }))

Note: As always the case with RxJS observables, we did not execute the selectors but only described their behaviour. Nothing will be executed and no computation will be performed unless you .subscribe() to the selector observables.

Memoized Selectors

Several patterns in RxJS exist to create the "memoized selectors" known from Reselect. The most prominent one is the ReplaySubject, we could easily use it to cache our customerModel from the above example:

const customerModel: Observable<Customer> = /* ... see above */;

const memoizedCustomerModel = new ReplaySubject(1); // passing 1 as the cache size
customerModel.subscribe(memoizedCustomerModel); // we eagerly subscribe, i.e fetch the data whenever the state changes

Alternatively, we could use publishReplay(1).refCount() to achieve similar behaviour:

const memoizedCustomerModel = customerModel.publishReplay(1).refCount()

Not that the above example using publishReplay(1).refCount() will not be eager, but lazy: Until a subscription arrives, no network request is made. When a subscription is added, all future subscribers will receive the same cached values until all subscriptions are disposed again. This is one of the most powerful patterns to load data from a server only when required to.