Aurelia Store and Immer

Posted on 2020-08-09 in Aurelia

I spoke last year of Aurelia store in this article At the time, it wasn't possible to combine Aurelia Store and Immer. The root cause of the issue was fixed in the latest version of Immer (7.0), so we can now combine both.

Let's study how to do this:

  1. Install immer with npm install --save immer@^7.0.7 or yarn add immer@^7.0.7

  2. Because we need Aurelia to update the state when it's used in component to add some observers, we must disable immer's auto-freezing. You can do this in main.js or app.js with:

    import {setAutoFreeze} from 'immer';
    // This is required to allow Aurelia to add its observer on objects in the state.
    setAutoFreeze(false);
    
  3. Update you actions to use produce. This will allow you to modify the state directly without mutating it. This way, the history features of the store will be preserved. For a simple example:

    // Before.
    export function selectBackend(state: State, selectedBackend: SelectableBackend) {
        const newState = {...state};
        newState.selectedBackend = selectedBackend;
        return newState;
    }
    
    // After
    export const selectBackend = produce((state: State, selectedBackend: SelectableBackend) => {
        state.selectedBackend = selectedBackend;
    });
    

    Thanks to immer, you don't have to copy the state explicitly, immer will create a new state based on your modification automatically. The produce function takes a function that will receive the draft of the state you can modify as first argument and all other arguments passed to you action as extra arguments. So the function you need to pass to produce takes the same arguments as a standard action. You still need to be aware and pay attention to some details:

    • All the objects in the store are changed so immer can track the changes without modifying the original objects. This means that this won't work:

      export deleteArticle = produce((state, article) => {
          // Will return -1 because article is not in the articles array, it's proxied version is.
          const indexOfArticleToDelete = state.articles.indexOf(article);
      });
      
    • Only the state will be a "draft" (to use immer terminology) and can be modified directly. If you were to modify other argument, you would mutate them. And if they are in the store, you would update them the store as well. So this won't work as expected and will mutate the object:

      export markAsRead = produce((state, article) => {
         article.isRead = true;
      });
      

    For both these issues, you need to first find the proper object in the draft state and apply the modification to it. You can view an example here.

If you want more detail, you can check this commit of one of my project when I updated my actions to use immer.

As noted by Vildan Softic (the creator of Aurelia Store), you can organize how you update the state differently to update the state correctly while keeping things clear without requiring immer. You can see an example here. Many thanks to him for pointing that out and giving me some pointer to correctly test Aurelia Store with Immer.

So while I think it's really nice to be able to use Immer, whether to use it or not highly depends on your programming style and on whether you can afford adding yet another library to your project (immer is about 15kB minified which is tiny, but it adds up with the rest).