Aurelia Store and Immer / immutable state


#1

I think I may have misunderstood what immer provides. My intention was to use immer to make the state objects immutable so that if I inadvertently bind a two-way or some other input directly to the state, then I’ll get an error (then I’ll fix it).

If I don’t do:

setAutoFreeze(false);

then the above happens if I don’t create a temp copy of my state in the vm when attaching the state directly to an edit input, then it throws an error like:

aurelia-binding.js:1409 Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'

Good. That is what I think I was expecting to happen. But if I leave autoFreeze turned on, then I am bombarded by the warnings in the console from Aurelia (and not just for two-way binds, I get that as soon as I do something like bind to a custom element (which should bind one-way?)

 <some-custom xxx.bind="state.identity.name.whatever"></some-custom>

stevies:

WARN [property-observation] Cannot observe property ‘name’ of object

Not so good. Those would potentially mask other warnings.

And then, in the immer docs, I see that:

Immer automatically freezes any state trees that are modified using produce. This protects against accidental modifications of the state tree outside of a producer. This comes with a performance impact, so it is recommended to disable this option in production. It is by default enabled. By default it is turned on during local development, and turned off in production. Use setAutoFreeze(true / false) to explicitly turn this feature on or off.

So not sure what immer is doing for me in production if that immutability is turned off?


#2

Maybe to help clarify, in the example at: https://github.com/zewa666/aurelia-store-examples/tree/master/immer - I was expecting if I added, say an input to the app.html file:

<template>
  <h1>For Immer Aurelia-Store</h1>

  <button click.trigger="decrease()">-</button> ${state.counter.count} <button click.trigger="increase()">+</button>

  <input type="text" value.bind="state.counter.count">

</template>

that I would get an error if I tried to type in a new value for the counter since I am now modifying the state directly.

With:

there is no error - it happily allows me to change the state from the view without going through the action (and that would be a bad thing as nothing else would get notified via the Store subscriptions).

With:

I do get the error:

Better. I see the error, and modify my code to remove the bind to the input, and it works OK again with the buttons triggering the actions to do the update to the Store state.

However, with setAutoFreeze(true); my console has lots of warnings from Aurelia:

And the setAutoFreeze(true); mode is not recommended for production.

Apart from the nicer syntax using the produce() function rather than Object.assign, I am not 100% sure what immer is doing for me if it is not making the state immutable?


#3

So in short what Immer.js has to offer for Aurelia-Store in my opinion is exactly produce(). Thats pretty much it. This is also enough for most cases because:

  • Handling cloning is error-prone (forgot to Object.assign to empty {} or shallow/deepclone)
  • It allows you to operate with normal mutations on the draft thus not having to learn new APIs (array.push(newItem) vs array = […array, newItem]
  • Strong typed (compare this to the string hell of Immutable.js)

So in short the benefit of Immer.js is simplyfing how you write actions.

function increaseCounter(state: State, newItem: any) {
  return produce(state, (draftState) => {
    draftState.myArray.push(newItem);
  });
}

// vs
function addItem(state: State, newItem: any) {
  const clone = Object.assign({}, state);
  clone.myArray = [...clone.myArray, newItem];

  return clone;
}

The additional feature of freezing an object is nothing else as a deepFreeze of your state

function deepFreeze(object) {

  // Retrieve the property names defined on object
  var propNames = Object.getOwnPropertyNames(object);

  // Freeze properties before freezing self
  
  for (let name of propNames) {
    let value = object[name];

    object[name] = value && typeof value === "object" ? 
      deepFreeze(value) : value;
  }

  return Object.freeze(object);
}

The problem here though is that Aurelia expects to add tracking information to objects so this pretty much won’t do it for complex objects.


What you seem to be really looking for though is a kind of warning if you by accident modified your state via two-way bindings. And this is where the clash happens. You actually should never two-way bind to a state object directly. Exactly because of the modification reasons.
Instead, given the counter example where let’s say you want to update the counter to a specific value vs toggling up/down you’d

  • Create a property, acting as local components state e.g. public theCounter
  • Two-way bind to theCounter
  • create a dispatch method which will update the state with the value of theCounter

This way you separate the global stores state from your local components state. The later is only used temporary to store the users input and pass it on to a dispatch. Mixing local and global state is where most troubles begin.

Binding to state objects is ok for one-way (VM to View) or one-time, or for interpolations, e.g repeating over an array of something.

I’ll be updating the plugins docs to describe those best-practices with a short sample


#4

Exactly. I know not to bind to the state directly with 2-way binding and follow the pattern of creating a temp object that any edit form should bind to for updates. But if I forget, or another dev does it anyway, I was hoping that the state objects would be immutable; an error thrown; and code gets fixed.

But if this cannot happen with Aurelia - and I think I can see why (Aurelia adds its own decorators to the objects to track / observe them) then are we really saying that we can’t use the immutable feature of immer (or any other of the many libraries that do that kind of thing) with Aurelia and that we just have to take care not to do bad things?

So we can’t ever do setAutoFreeze(true) with immer and Aurelia? If so, not a problem really - but then I lose the major benefit of immutable state (which I thought I was getting).

Thanks for clarifying.


#5

I see where youre going. So essentially if either Aurelia would track objects different (potential vNext) or bindings would understand the concept of a global state it could throw an error. Either way would solve the intended use-case right?


#6

Yes. The problem I see with the central store and access only via actions is that it is not enforced by anything other than convention and people following the correct pattern. Hard to track bugs are very easy to introduce in a large code-base if someone or something updates that state when it should not do so. I was hoping that immer would provide an easy solution for enforcing the correct pattern (or at least throwing error is someone breaks it).

Immer still looks useful for what it does provide.

But thanks for taking the time to clarify things for me.