Deep Cloning in Redux State
A recent frontend performance issue surfaced: interactions became sluggish and eventually blew up memory. The culprit was, unsurprisingly, us—and the weapon of choice was
cloneDeep
.
Consider this reducer:
const updateDetailReducer = (state: IDetailState, action) => {
const detail = _.cloneDeep(state);
return { ...detail, ...action.params };
};
Does it work? Functionally, yes—the state updates as expected.
But it’s terrible for performance because of the deep clone. Every property inside detail
becomes a brand-new object. If dozens of connected components read detail
or its children, the page crawls. The more interactions, the worse it gets. Remember: when component props change, React re-renders. Most of those props didn’t change, yet deep cloning forces every related component to re-render, wasting cycles and killing performance.
shouldComponentUpdate
Deep cloning triggers a storm of re-renders because React’s shouldComponentUpdate
performs a shallow comparison (basic types by value, objects by reference). Fresh references everywhere mean React thinks everything changed.
Lodash cloneDeep
Since we used Lodash’s cloneDeep
, let’s contrast it with clone
and the native JSON.parse(JSON.stringify())
. I’ve hit errors deep cloning with JSON.parse(JSON.stringify())
that cloneDeep
handled, so they definitely behave differently.
cloneDeep
Iterates recursively:
const a = {info: {name: 'xxx'}};
const b = _.cloneDeep(a);
console.log(a === b); // false
console.log(a.info === b.info); // true
console.log(a.info.name === b.info.name); // true
clone
Non-iterative copy:
const a = {info: {name: 'xxx'}};
const b = _.clone(a);
console.log(a === b); // false
console.log(a.info === b.info); // true
JSON.parse(JSON.stringify())
Similar to cloneDeep
, but only works with numbers, strings, and plain objects without functions or Symbols.
So for pure deep clone behavior, cloneDeep
is more complete and safer.
const a = {info: {name: 'xxx'}, formatter: () => this.info.name + '@'};
const b = JSON.parse(JSON.stringify(a));
console.log(typeof a.formatter); // function
console.log(typeof b.formatter); // undefined
Final Thoughts
Fixing the bug was quick, but the lesson sticks.
- Redux uses a single state tree. Each reducer updates part of that tree, but that doesn’t mean you should clone the entire branch.
- Deep clones aren’t forbidden in reducers—but they’re rarely necessary. The higher up you clone, the more components you invalidate. Use them sparingly.