Some thoughts on Aurelia Store

Posted on 2019-08-24 in Aurelia

On my spare time, I am developing a board game that uses the Aurelia framework. I currently manage the state in a service without any dedicated state management solutions (like RxJS, Aurelia store, Redux, …). And I feel I am reaching to the limit of what I can do with it and feel the need for a dedicated solution. So I decided to give aurelia-store the official plugin for state management for the framework. I did so in at test project: aurss a small app to list rss feeds.

First, let me explain a bit why we would need a store: in Aurelia, you can use services (ie instances of a object) that are injected into your components. You can use them for actions (eg interaction with an external API) and to store data. If you app is big, you will end up with many services. You'll have to keep them all in sync (maybe with observers or the event aggregator) and make sure your component are correctly updated.

It turns out this approach can be quite tricky and that's why solutions like Redux (from the React world) or VueX for Vue.js and of course Aurelia store have emerged: to help you manage the state in a single place so you don't have to do the hard work of keeping everything in sync. Instead of having multiple services, you have one store, you rely on functions to change its state and the store "notifies" your components that something changed and that they must be updated. It also comes with some nice features like easy persistence and recovery from local storage and time travel.

Aurelia store more specifically is a plugin for Aurelia to help you use this pattern with the Aurelia framework. It's based on RxJS a popular library for event programming. The idea of RxJS is register actions to do when a given event occurs as specified by the observer pattern, ie do something like this:

const observable = fromEvent(document, 'click');
observable.subscribe(() => console.log('clicked!'));
observable.subscribe((event) => sendEventToAnalytics(event));

I'll let you see the doc of RxJS to learn more about that.

What's very interesting about Aurelia store is that while based on RxJS (and its full power), it remains easy to use at first: you don't need to know RxJS to start and don't need much knowledge to do even quite advanced things. Please read the introduction to Aurelia store to have a more detailed explanation about why the plugin exists and its relationship with RxJS.

Let's see briefly how to use it (I assume you are already familiar with Aurelia, if not, the official documentation will give you more details on how to integrate it in your app):

  1. In your main.js file, you must enable the plugin and define an initial state:

    aurelia.use.plugin('aurelia-store', { players: [] });
    
  2. Inject the Store in your components:

    import { inject } from 'aurelia-framework';
    import { Store } from 'aurelia-store';
    
    @inject(Store)
    class MyComponent { 
    
  3. Listen to events with:

    // `this.state` will receive a new value each time the state is updated.
    // This way, the component will re-render.
    this.subscription = this.store.state.subscribe((state) => this.state);
    
  4. Dispatch actions to update the state:

    function addPlayer(state, player) {
        // Don't mutate the state directly, create a new one. See the doc for why.
        const newState = {..state};
        newState.players = [...newState.players];
        newState.players.push(player);
        return newState;
    }
    this.store.registerAction(addPlayer.name, addPlayer);
    setInterval(() => this.store.dispatch(addPlayer, "Player " + parseInt(Math.random() * 100, 10), 10000);
    

Astuce

Don't forget to dispose of subscription with this.subscription.unsubscribe. See the doc.

I must say the experience of using Aurelia store is very pleasant: the plugin is easy to integrate, it works well, it's not hard to understand, it seems to play nice with RxJS if you need some advance features (for instance to register to updates to part of the state or to get only the first change).

If I compare this to Redux which is the other state management solution I used, I think that using directly functions when we dispatch actions is a very good idea: instead of having actions and reducers like in Redux, you only have one concept to achieve the same result. It's also way easier to get started and is easier to understand (each time I have to touch a Redux code base, I have to re-understand how it works and what the link between actions and reducers is).

The fact that the plugin hides the complexity of RxJS is also a really good thing. By comparison with Angular (from the tutorial I made at least) where RxJS is fully exposed, it really simplifies the development and startup.

So I am really pleased with the plugin, what I can do with it and the overall developer experience it provides. There are some small gotchas though:

  • Don't forget to register your actions. You may forget at first. But the store will provide an helpful error message if you forget so it's not the end of the world.
  • Pay attention to async actions: unlike other solutions, the Aurelia store allows you to do something like this: await this.store.dispatch(costlyApiCall). If you do this, your app will freeze until the API call is done which is not what you want (and may not be noticeable locally if your API is on your machine). So for expensive async operations, split the actions in two: one to do the call and one to update the store when the call is done. To view an example of this, look at fetchArticles and receivedArticles from my test app. But if you know your async actions will resolve fast (querying IndexDB for instance), this shouldn't be an issue.
  • Immer.js (useful to ease the writing of state mutation) doesn't work with the plugin out of the box yet (see this discussion which also gives a possible solution and this issue). Maybe alternative solutions work (like immutable.js) but I haven't tried them. Update of 2020-08-09: it is now possible, see this update.

Further readings: