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.