Using Aurelia Store what is the best way to copy your state within Actions?


#1

What are some ways in which to deep clone my state inside an Action?

Possible solutions I’ve seen are Immer.js, JSON.parse(JSON.stringify), or create a recursive function which does a deep clone. I was even thinking of recreating what Redux does and create Reducers and combine them for my entire state but there are reasons which I decided against it. I like in Aurelia store how I can push Actions to the store, inside each Custom Element’s bind() method. I believe this is a better way because it promotes high cohesion and low coupling. Whereas with Redux I had all my Actions and Reducers centralized with the store.

I’ve never used Immer.js and I was wondering what are some Pros and Cons of using it?

I like the idea of using JSON.parse(JSON.stringify) because it ensures my state is serializable at all times. Which will be good if I want to persist my state to some type of storage. It allows me to identify problems early like when I tried to use Set and Map object types and I quickly found out that those don’t serilize/deserialize with the above methods. But I’m wondering if my store grows too large will JSON.parse(JSON.stringify) become too slow?

Then there is the option of just creating a recursive function which copies my state. Has anyone tried this?

Thanks


#2

Hey there. Json.parse(json.stringify) certainly is good enough for most common apps. As you I like the fact that it guarantees serializability. Immer.js essentially is just a wrapper making sure you dont mutate the original obj.

If youre looking for perf comparisons take a look at https://github.com/raml-org/raml-js-parser-2/issues/814, https://gist.github.com/izy521/4d394dec28054d54684269d91b16cb8a or https://www.npmjs.com/package/deepcopy this alternative.

One additional thing to think of. You dont always have to fully clone the state. As long as values do not change, youre fine with a shallow clone. E.g. lets Imagine you have a complex object representing the crud permissions from the user, loaded from the backend which is part of your state. You likely wont change it so a deep clone here is unnecsssary. As such your states structure might dictate whats the best cloning strategy. It does certainly require more discipline though. Make sure you bind to “readonly” values only one-way.

Building upon that, the module slice approach, as depicted here https://stackblitz.com/edit/aurelia-typescript-y3x3ca, might further increase reusability of the state. If youre in Module A and never mutate Module Bs part, just shallow clone it straight away.

As above demonstrates, it highly depends on your apps structure and discipline on how to scale your app. Lets also not forget that maybe not everything should go into your state straight away. E.g. above crud settings example. If its readonly you might just keep it as a local state of a service with easier access via DI or selectors.


#3

Your question also contains a lot of good sub-hints. With regards to making sure your state is always serializable you could create a middleware which is added after the dispatch manipulation only in devmode. It would do json parse/stringify and throw an error if somethings wrong. On prod you wouldnt notice the perf hit.


#4

Hi zewa666. Do you have an example of that middleware somewhere?

I am also trying to put together an application using Store, and want to try to get off to a good start with best practice. I am trying to follow the pattern in your services example - https://github.com/zewa666/aurelia-store-examples/tree/master/services - is that the recommended approach - keeping the viewmodel fairly dumb, dispatching actions that know what services to call on the backend, and then update the store on the response? Essentially, business logic is also in the actions?


#5

Hi @stevies. Tbh best practices should evolve over time, right now its just always a bit of guessing thats part of it :slight_smile:

I think the linked approach is fine if you like to still keep services around, which I personally do like. Also keeping the Business Logic inside actions is a way to separate concerns. Dumb/smart components is also a pattern vivid in the redux/react domain. So id say its a good fit to start with. Just stay open minded and dont be shy to change your structure if you find something better. I did three rewrites of how I handle state in a major app Im working on and each phase had their pros at the certain size of the App.
Nothings forever, so count your days as a song by the band Nevermore goes which I like to remind myself everytime I think now i found the silver bullet :slight_smile:


#6

Thanks. If you ever get time, maybe you could share those experiences a little - not necessarily here (probably good to have some notes in your git project). In very basic terms: if small app - handle state like A. Bigger app - best to do it B way. Huge app (or app that does X) - you really want to do it like C. Maybe list pros and cons of A, B and C.

I know there are no silver bullets. If there were, we would be out of work.


#7

I have tried using immer (attempting to ensure that the state when it gets bound into my view does not get accidentally changed if I forget to manually clone it or bind-one-way every time). However, I am seeing lots of warnings:

WARN [observer-locator] Cannot add observers to object 
WARN [property-observation] Cannot observe property 'name' of object

All still appears to work OK. But those warnings need to go - they will mask other possible problems.
I don’t see that in the example (even when I change from simple bound number to and object. Anything likely that I doing incorrectly?


#8

Have you frozen the object with immer? Aurelia needs to add private Info for tracking which doesnt work if the object is sealed


#9

Thanks. Hmmm - not sure. I did setAutoFreeze(false); as I the example app - but maybe not everywhere. I’ll go back and look at that.

My objects in the store are nested:

user: {
   identity {
      name: {}
   }
}

I assume not locking does not break immutability. Need to read the docs on immer :-}


#10

@stevies with regards to the middleware here is a sample:

Run the sample and open up the DevTools (F12) to inspect the console


#11

Excellent. Thanks.

Setting setAutoFreeze(false); on every action removes the warnings.


#13

I’d recommend we create a new topic about the Store and immer.js to not pollute this one if youre fine. Just use your last comment as the Intro there. Thx in advance