I'm new to ngrx (and never used redux) and am trying to make sense of it all - especially whether you need deep copies of the state. Here's what I've learned so far and what still confuses me (further down in bold).
The ngrx docs state that
Each reducer function takes the latest Action dispatched, the current state, and determines whether to return a newly modified state or the original state.
They also point out that state transitions need to happen immutably - if you change state, it needs to be on a copy:
Each action handles the state transition immutably. This means that the state transitions are not modifying the original state, but are returning a new state object using the spread operator.
They don't say why that is necessary, though, beyond promoting referential integrity (and I'm not quite sure how making sure your references are pointing to data - I only know the term from relational database contexts - plays into it all):
The spread syntax copies the properties from the current state into the object, creating a new reference. This ensures that a new state is produced with each change, preserving the purity of the change. This also promotes referential integrity, guaranteeing that the old reference was discarded when a state change occurred.
That (referential integrity and the section above) doesn't really explain why that is necessary, though.
Elsewhere on SO, I found a comment suggesting it allows to change Angular's change detection strategy to OnPush
.
(I was also somewhat baffled by how, if an action can trigger several reducers, the resulting state copies can be consolidated, but that's apparently explained by each reducer exclusively looking after a separate slice of the state and redux being aware of that.)
The important thing seems to be, though, that a copy - shallow or deep - of the state creates a new reference, and that means ngrx is pushing a change to its subscribers:
If the returned object reference has changed, it will trigger any related RxJS state subscriptions for the particular piece of state in question. Which subscriptions are triggered can be minimised using some good ngrx selectors.
Generally speaking, the Redux FAQ have a list of why immutability is a good thing and why redux requires it:
increased performance, simpler debugging, easier reasoning, safer data handling, shallow equality checking
They also say that it
enables sophisticated change detection techniques to be implemented simply and cheaply, ensuring the computationally expensive process of updating the DOM occurs only when it absolutely has to
As just pointed out (as one of the requirements for immutability) redux does shallow equality checking.
Nrgx's docs, however, recommend deep cloning (plus, the state isn't truly immutable if the copy references old objects, I suppose):
Note: The spread operator only does shallow copying and does not handle deeply nested objects. You need to copy each level in the object to ensure immutability. There are libraries that handle deep copying including lodash and immer.
However, deep copies might have "nasty" side effects (when using the cloned items in an Angular component, say):
This question even extends to the way Angular's
ngFor
change detection works (and using atrackBy
function complicates that even further!): when I clone every item inThing[]
and have my reducer return a new list of clonedThing
s, Angular will think it's a brand new list (which it technically is) and run change-detection for all items in the list: They will also be brand new, and as such, old list items get removed and new ones get added to the DOM.Suppose you have a
ThingComponent
for eachThing
in thengFor
list. In that component,ngOnChanges
will fire. But here's the thing: theSimpleChanges
passed tongOnChanges
will never containpreviousValues
, because the whole list got replaced, and so there is previous value: everything is brand new, from Angulars perspective.
The author also points to a solution (trackBy
), but I'm now wondering:
is using deep copy really a good idea with ngrx (and do you really need deep copies if all that is required to make the library work is a new object reference for the root/state object)? The last quote sounds a bit like it would be a better idea to only swap the root object, the state, out, to get a new reference that then triggers subscriptions, but leave the nested objects - lists, especially - alone.
Our goal to keep our apps as fast as possible what means to reduce calculation when it's not needed or redundant.
Angular has 2 change detection strategies onPush and Default, the first one checks pointers of variables passed into inputs, the second one does deep check and quite heavy on heavy objects. But they have one thing - they rerender only when data has been changed.
Deep copy is bad because it causes the same data to be presented under new object pointers, this causes render cycles and because data is the same rerendered result will be the same too, unless it's time dependent app.
The best way for the data flow is from top to bottom, and bottom can notify top to change the data.
In such circumstances we can always find how data came here via checking parents and what they do with the data, and we have a single point where data is changing and we can find there who causes the change.
If we mutate data where we need the change instead of notifying the top, with time we can't easily find who does it anymore because these places will be everywhere in the code and we need to check all of them to find the issue.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With