Advanced Reactivity: combining and chaining observables
Some common patterns around reactivity, advanced use-cases of combination and chaining of observables, using Svelte.js stores
In my previous article, I explained our decision to move forward with Svelte.js for our production website. In this article, I am diving into a technical detail we had to build for the application: chained stores. Svelte’s developer experience is built around reactive stores: using the observable pattern for dynamic values in the UI. This means that once a value changes, the pieces of UI that depends on it update to reflect the new value in the DOM.
What I am calling a chained store is a store that needs the value of a first store in order to exist. For example, a first store that defines the user id, and the second one that needs to point to some attribute of the user. If the user id changes, we need to renew the second store.
I will first remind the patterns that exist in svelte.js: stores and derived stores, then dive into a real-life example of chained stores.
While I'm focusing on Svelte stores, a simple implementation of observables, the concepts could easily be transcribed to fit other implementations.
A store in Svelte.js is basically an object implementing the observable interface: having a subscribe method, being lazy to evaluate, returning a unsubscribe method when subscribed to.
The beauty of svelte’s developer experience comes out when using stores in your UI components, with subscription and unsubscription to stores handled by the compiler:
Notice the ‘$’ character in front of the variable we imported: that’s the trick that lets the compiler know that we are using a store, and it will handle subscription/unsubscription when the component is mounted and unmounted. Just with that, you can use the store as if it were a variable ! (as long as you use it inside a “reactive declarations”, more on that in the tutorial). I find this to be a great developer experience — what Svelte.js is all about.
You can play with this code in this REPL https://svelte.dev/repl/1d1d4f0beb4240848fa04be97ffbdf6c?version=3.29.4.
A common pattern is also to derive data. As in the example provided above, you will sometimes need the raw value, and sometimes the formatted value. Derived stores are a simple way to factorize transformation code. Derived stores also allow to merge multiple values into one. For example, see in this example how I created a “progressStore” from the values of the form being filled out:
The code of this progress store is very simple:
As a reminder, the derivation code runs if and only if:
- There is at least one subscriber to the store.
- One of the values from the original stores changes.
So much for performance concerns! Our code will only run when the progress bar is displayed on the user’s screen.
Here is the REPL: https://svelte.dev/repl/ea03956ee4374c8cbe078dcaed40d5bf?version=3.29.4
When it is not enough
The writable, readable and derived stores that are provided by Svelte.js give a lot of flexibility. When creating them, with their simple interfaces of initialization and unsubscription, and handling of asynchronicity, you’ll be able to plug a lot of different data sources seamlessly to your application. For example a web-socket, a polling mechanism, a custom or open-source library, … all will be exposed to your application as a store and very well integrated with the rest.
Yet, we still quickly came upon a use-case that wasn’t straightforward to handle, and the center of this article.
As mentioned in my previous article, https://strollyn.com is using Google Firebase for the backend, as well as the hosting. Cloud Firestore, the database, is a No-sql, document-oriented database. As such, we chose to de-normalize the data model, that ended up looking like the following, with a user able to own several homes:
When the user first logs in, we get his uid. In order to get his home and expose it as a store, we’ll have to first look into the content of user document in order to get the homeid, then subscribe to the proper home document. In other words, we have to wait until the uid is known to even create the home store. This is where the derived method falls short: it can only be used with stores that exist at the time of declaration.
Enters the chaining of stores
There are several ways to approach the issue, but we chose an approach that would respect a functional way of chaining function calls. We named it chainReadableStore, and this is how it is used.
First of all, let’s create our uidStore, that gets filled when the user logs in.
The main thing worth noticing is the call to set(user.uid), line 7, once the user object is available.
We then want to create a store that returns the homeid of the first home, but only once the uid is known!
Finally, we want to chain again that value, to get the home document as soon as the rest is available!
On the UI side, all the complexity is hidden. The stores can be used as in the beginning of the tutorial, for example to display the picture of a home, and we can seamlessly “wait” for the info to arrive from the server.
- This function creates a readable store that subscribes to the initial store(s)
- When the initial store(s) values are different than undefined, it will call the callback function to create the chained store, that is ephemeral.
- When the value from the initial store(s) changes, it will unsubscribe (and therefore destroy) the ephemeral chained store, then create a new one, using the new value.
- When the store has no more subscribers, it will destroy its subscriptions as well as the ephemeral chained store.
With the previous example of users and homes, the chaining function is used to create 2 new stores. When the uid is first known, it allows to get the homeid (not right away though, but after some hidden server requests). Once the homeid is known, we know where to fetch the home document. Because svelte stores only notify subscribers when the value actually changes (see this REPL if in doubt), our function only runs as often as it needs: when either the uid or the homeid changes, and only if a subscription to the final store exist.
Thanks to the chained stores, we have a very simple and flexible way of defining dependencies. It can help a lot when dealing with the loading of the application. On the UI side of the code, it integrates very nicely, thanks to the store API.
You can fiddle with it in the REPL.
A word of warning
Even though Svelte stores make things quite safe (lazy evaluation, only notify when values actually change), keep in mind that chained stores imply a cycle of unsubcription/subscription every-time the initial store has a new value. In case of several chaining in a row, it can create a lot of operations. I find it particularly adapted to an app initialization, but need to give a warning: do not use it in hot code paths, or for stores that regularly change values. When there is no dependency between stores, stick to derived stores.