When creating a reducer function in ngrx, everywhere I read says that I should return a copy of the original/previous state. Either by using spread operators or by using a library or tricks like JSON.parse(JSON.stringify(state))
.
But I found one catch there and I couldn't find anyone talking about it.
The last state returned in a reducer is the state that's going to be shared with all current subscribers and with future subscribers too.
That means that all components that use a certain store will see the same state object.
That also means that if any value in the state is changed in one component (without dispatching an action), the store will actually have the value modified, but the other components won't be notified.
What's the point in returning a copy of the current state if it's going to be shared everywhere?
The word immutable is used all the time, but that state is not immutable at all, because the store returns its own inner object, and not a copy of that.
I understand if the immutable part is a concept that needs to be followed by the developer. But then, the copy of the original object/values needs to be done in the component that uses it. Returning a shallow or deep copy from the reducer seems to be just waste of processing power and memory.
I'll try and answer.
A reducer in pesudocode looks like this:
myReducer(state, action) {
switch(action) {
case ACTION_1:
return {...state, prop: action.payload}
case ACTION_2:
const newState = _.cloneDeep(state)
newState.prop = action.payload
return newState
default:
return state
}
}
In case ACTION_1 you are not mutating state. The spread operator creates a new object with a new reference and the new reference is what is needed to signal a change.
In case ACTION_2 you are cloning the state. You mutate the cloned state and return it. Because the cloned state has a new object reference it signals a change has been made and everyone is happy.
In the default scenario, any other action (e.g. ACTION_3) is ignored and the original state is returned signifying that state has not changed. The object reference has not changed and thus "no change" is signalled (this is why it is important not to mutate the original state).
When an action is fired off, the action is passed to EVERY reducer. And thus, reducers that don't want to modify their associated piece of state can ignore the action by relying on the default case statement.
A single action can, and often does, trigger state changes in multiple reducers.
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.
PS There's a great library called ngrx-store-freeze which will enforce the "no mutation" principle. It will throw an error early if you mutate state. This helps to avoid hard to track down bugs. Hook into store freeze with a meta reducer.
PPS The whole purpose of using the object reference to determine change is because it is much faster to check an object reference than it is to check every value on an object to see if it has changed. This is why immutability is so relevant.