[SOVLED] Should data be mutable or immutable?

Hi,

I’m using Aurelia since about 1 year on my free time, and I really love it. I’m mostly a backend developer (Python) but I’ve a small professional experience (1 year) with React/Redux.

Everything in Aurelia has been easy and pleasant to learn for me, except one point that I still struggle with : should I manage my data in a mutable or immutable style?

I understand the reactive binding as something like this: for example for an array, override the .push, .splice … to spy how the array is modifed and be able to update the DOM accordingly. So I would modify an array with mutation (.push, .slice…).

However, @computedFrom seems to compare its argument based on immutability, ie for example when a array is reassigned: values = [...values, newValue]instead of values.push(newValue).

I also browsed the Aurelia 2 documentation, and it didn’t make it clearer for me.

Here is an CodeSanbox example where an array of number is displayed, with its sum. With the immutable version, the sum is correctly displayed but it looses all the reactive binding advantage when displaying the array.

I know that they are workaround like incrementing a number after each array modification and doing something like @computeFrom(values, version), but I would expect a more direct solution.

In fact, I would have except everything working with mutable data, and @computeFrom detects changes like reactive binding does (“syping” the .push, .splice etc), so the different framework parts would have been coherent.

Could you elaborate on what’s the opinion of Aurelia on the data mutability? I would love to see in the doc : By default with Aurelia, you must manage your data in a (im)mutable way, and as soon as the developer correctly manage the data, everything (the DOM, the computedFrom) are correctly udpated.

Thanks is advance!

2 Likes

The DOM is mutable by design and does not need to be re-created for each change. We follow the same principle.

Immutability (or more broadly speaking, re-creating state for each change) should only be applied when there is a functional justification for doing so, for example an undo/redo capability. You can’t really solve that problem without creating (and storing) multiple copies of your state.

A simple rule of thumb follows from that: if you’re creating a new state without storing the old one somewhere, you don’t need to create a new state.

Creating a new copy of a state just to trigger reactivity is the wrong reason and should be considered a workaround. Maybe this is necessary in some very specific edge cases but consider it a bug or shortcoming in Aurelia if it’s really the only way to do something. In the case of your example, add array.length to the tracked property names. In v2 this will work automatically by the way.

Good point on adding it to the docs, we’ll think about a good place to put this :slight_smile:

3 Likes

Thanks for the very clear reply! So Aurelia does not encourage to modify the data in a immutable way to take advantage of shallow comparison (I was wondering if it was the case for computedFrom).

I rewrote my example in a new codeSandBox (a list of object, where I modify a property of one of the object, and want computedFrom to detect it) to be closer that my real application.

Maybe aurelia-deep-computed could be helpful for me (found on a thread about a similar issue).

So the Aurelia 2 approach would be to provide an expression or function to computedFrom / watch so that is is able to detect deep change, but the approach will not be to do shallow comparison?

I think I have to read the Aurelia 2 doc more carefully…

1 Like

In Aurelia 2 getters automatically have their dependencies tracked so you don’t need to explicitly provide an expression. We use some tricks with proxies to accomplish this. No need for computedFrom.

watch uses that same mechanism to figure out the dependencies of the function you pass in.

W.r.t. your example, I believe it’s still on our todo list to make that work automatically.

But what you could do is to @computedFrom('data.items.length') and then add a no-op array mutation to the item mutation to trigger observation:

edit(item, newValue) {
  item.value = newValue;
  // remove and add the last item
  this.items.push(this.items.pop());
}

A less hacky way would be to use the binding signaler but that’s a bit more boilerplate. That’s described in the docs though.

FYI that data.items.value doesn’t do what you think it does. That actually tries to observe the non-existing value property on the array itself. You could technically make it observe at least one item like that by doing data.items[0].value but that really doesn’t make sense. It’s just to illustrate how it works: actually evaluate the real javascript expression to get to the value you need. If you can’t, then it can’t be observed either, at least not in v1.

2 Likes

About the @computeFrom('data.items.value')you’re right, I forgot to delete it…

I think I understand the idea with the proxy. (In my example, when first computing the sum, the proxies would inform Aurelia that all the item.value have been read , so when they are modified, the sum must be recomputed).

Thanks again for the replies, I understand my problem better now!

1 Like

This help request is marked as solved already, but I still would like to express my point of view here.

There are many different answers to such kind of design questions, and I think that in many circumstances, one answer is not per se better or worse than the other, and certain design choices tend to depend on the level of experience and insight of the developer(s)/designer(s) who are involved.

Personally, I think that immutable objects have both advantages and disadvantages. I tend to prefer immutable objects and support Marijn Haverbeke’s vision in this regard: persistent (immutable) data can make complex software easier to reason about, but on the other hand, such software can be more difficult to design well. For a more detailed explanation, you could read the section “Persistent data” in chapter 7 of his book “Eloquent JavaScript”. It’s a rather short section, so it will probably take just a few minutes of your time. (However, in my case it took me somewhat longer, since I got hooked on the author’s style of writing, which made me read his whole book… :wink: )

2 Likes