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.