Reputation: 43
I'm trying to code the reducers in my app a bit more defensively since my team uses the pre-existing code as a template for new changes. For that reason, I'm trying to cover all the potential deviations from immutability.
Say you have an initial state that looks as follows:
const initialState = {
section1: { a: 1, b: 2, c: 3 },
section2: 'Some string',
};
And a reducer handling an action like so:
export default function(state = initialState, action) {
switch(action.type) {
case SOME_ACTION:
return { ...state, section1: action.payload.abc };
}
return state;
}
Then you could have a dispatcher that does the following:
function foo(dispatch) {
const abc = { a: 0, b: 0, c: 0 };
dispatch({ type: SOME_ACTION, payload: { abc } });
// The following changes the state without dispatching a new action
// and does so by breaking the immutability of the state too.
abc.c = 5;
}
In this case, while the reducer is following immutability patterns by creating a shallow copy of the old state and changing only the bit that changed, the dispatcher still has access to action.payload.abc
and can mutate it.
Maybe redux already creates a deep copy of the whole action, but haven't found any source that mentions this. I would like to know if there's an approach to solve this issue simply.
Upvotes: 4
Views: 2854
Reputation: 35501
It should be noted that in your example, the mutation isn't going to cause any problems if you just perform appropriate level-copy based on the object.
For abc
looking like { a: 1, b: 2, c: 3 }
you can just do a shallow copy while for a nested object { a: { name: 1 } }
you'd have to deep copy it but you can still do this explicitly without any libraries or anything.
{
...state,
a: {
...action.a
}
}
Alternatively, you can prevent mutation with an eslint-plugin-immutable which would force the programmer against writing such code.
As you can see in the description of the ESLint plugin above for the no-mutation
rule:
This rule is just as effective as using Object.freeze() to prevent mutations in your Redux reducers. However this rule has no run-time cost. A good alternative to object mutation is to use the object spread syntax coming in ES2016.
Another relatively simple way (without using some immutability library) to actively prevent mutation is to freeze your state on each update.
export default function(state = initialState, action) {
switch(action.type) {
case SOME_ACTION:
return Object.freeze({
...state,
section1: Object.freeze(action.payload.abc)
});
}
return state;
}
Here's an example:
const object = { a: 1, b: 2, c : 3 };
const immutable = Object.freeze(object);
object.a = 5;
console.log(immutable.a); // 1
Object.freeze
is a shallow operation so you'd have to manually freeze the rest of the object or use a library like deep-freeze.
The approach above puts the responsibility of protecting mutation on you.
A drawback of this approach is that it increases the necessary cognitive effort for the programmer by making mutation-protection explicit and is thus more prone to bugs (especially in a large codebase).
There could also be some performance overhead when deep freezing, particularly if using a library that is made to test/handle various edge cases that your particular app perhaps never introduces.
A more scalable approach is to embed an immutability pattern into your code logic, which would push the coding patterns naturally towards immutable operations.
One approach is to use Immutable JS where the data structures themselves are built in a way that an operation on them always creates a new instance and never mutates.
import { Map } from 'immutable';
export default function(state = Map(), action) {
switch(action.type) {
case SOME_ACTION:
return state.merge({ section1: action.payload.abc });
// this creates a new immutable state by adding the abc object into it
// can also use mergeDeep if abc is nested
}
return state;
}
Another approach is to use immer
which hides immutability behind the scenes and gives you mutable apis by following the copy-on-write principle.
import produce from 'immer'
export default = (state = initialState, action) =>
produce(state, draft => {
switch (action.type) {
case SOME_ACTION:
draft.section1 = action.section1;
})
}
This library might work well for you if you're converting an existing application that probably has a lot of the code performing mutation.
The drawback of an immutability library is that it increased the barrier to entry to your codebase for people unfamiliar with the library because now everybody has to learn it.
That being said, a consistent coding pattern reduces the cognitive effort (everybody uses the same pattern) as well as reduces the code chaos-factor (prevents people inventing their own patterns all the time) by explicitly restricting the way code is built. This will naturally lead to less bugs and faster development.
Upvotes: 8
Reputation: 67567
I'm a Redux maintainer.
Redux itself does nothing to prevent mutations to your state. Part of that is because Redux doesn't know or care what the actual state is. It could be a single number, plain JS objects and arrays, Immutable.js Maps and Lists, or something else.
Having said that, there are several existing development addons that catch accidental mutations.
I will specifically suggest that you try out our new redux-starter-kit
package. It now adds the redux-immutable-state-invariant
middleware to your store in development mode by default when you use the configureStore()
function, and also checks for accidentally adding non-serializable values as well. In addition, its createReducer()
utility lets you define reducers that simplify immutable update logic by "mutating" the state, but the updates are actually applied immutably.
Upvotes: 4
Reputation: 1475
I believe that within your reducer,if you create a new object from action.payload.abc,than you can be sure that any change in the original object won't impact the redux store.
case SOME_ACTION:
return { ...state, section1: {...action.payload.abc}};
Upvotes: 0