-
Notifications
You must be signed in to change notification settings - Fork 7
Computed values and selectors
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. 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).
On a reactive-state Store
instance, there are two basic methods to obtain the state and subscribe to state changes: .select()
and watch()
, which are very similar. Both will return an observable that emits upon state changes. Both cache the last emitted state, and will imediately emit the last cache value when subscribed (along with any other future changes until the subscription in unsubscribed). And both take an optional selectorFunction
(default if none provided: identity function) that can be used to only obtain a subset from the state:
const store = Store.create({
counter: 0,
todos: ["Walk dog", "Learn RxjS"]
});
// use .select()
let counter = store.select(state => state.counter);
counter.subscribe(n => console.log(n)); // output: 0
let todos = store.select(state => state.todos);
todos.subscribe(t => console.log(t)); // output: ["Walk dog", "Learn RxJS"]
// we could also use .watch()
counter = store.watch(state => state.counter);
counter.subscribe(n => console.log(n)); // output: 0
todos = store.watch(state => state.todos);
todos.subscribe(t => console.log(t)); // output: ["Walk dog", "Learn RxJS"]
The .select()
function will always emit a value when the state changed, even if the part of the state that you are interested in (selected by the selectorFunction argument) did not update at all.
The .watch()
function performs a shallow-equal test between two objects returned by your selectorFunction which you passed as an argument. If both objects are shallow-equal, no value is emitted.
Using the .watch()
function is thus the suggested way of obtaining state updates. Use .select()
only for debugging/logging, or pipe it through a distinctUntilChanged
operator that is better suited to decide if the state update should be emitted (if i.e shallow-equal is not feasible in your situation).
Deducing values from RxJS Observables is the core functionality provided by RxJS. Combining it with the state updates obtained via store.select()
or store.watch()
, 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 = store.watch(state => state.integers).pipe(
map(ints => ints.sort((a, b) => a - b))
);
sortedIntegers.subscribe(sorted => console.log(sorted));
// consolog.log: [1, 2, 3, 4]
const sumOfIntegers = store.watch(state => state.integers).pipe(
switchMap(ints => from(ints)),
scan((acc, n) => acc + n, 0))
);
sumOfIntegers.subscribe(sum => console.log(sum));
// consolog.log: 10
const uppercaseQuote = store.watch(state => state.quoteOfTheDay).pipe(
map(quote => quote.toUpperCase())
);
uppercaseQuote.subscribe(quote => console.log(quote));
// console.log: "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) should not go into the state (they are not plain objects and not serializable!) just as in the Redux philosophy. You would instead pass the computed Observables around (i.e. as function arguments, or by export/import
ing them).
We can use a customer id from the store, and map it to a REST response upon change:
const store = Store.create({
activeCustomerId: "1234",
/* ... */
});
const customerModel = store.watch(state => state.activeCustomerId).pipe(
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!
Several patterns in RxJS exist to create the memoized selectors known from libraries like i.e. Reselect. The most prominent one is 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)
and refCount()
to achieve similar (but not same!) behaviour:
const memoizedCustomerModel = customerModel.pipe(publishReplay(1), refCount())
Note that the above example using publishReplay(1)
with 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.