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.
Announcing NgRx Version 9: Immutability out of the box, customizable effects, and more! Today we're happy to announce the version 9 release of the NgRx platform. This release contains new features across, bug fixes, and some breaking changes, all aimed at improving the developer experience when using NgRx libraries.
Reducerslink. Reducers in NgRx are responsible for handling transitions from one state to the next state in your application. Reducer functions handle these transitions by determining which actions to handle based on the type.
The forRoot method is invoked in the AppModule and, generally, once in the application to initialize the Store and provide the initial reducers/actions/state configuration.
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.
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