Deep Cloning in Redux State

· 2 min read · 355 Words · -Views -Comments

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.

  1. Redux uses a single state tree. Each reducer updates part of that tree, but that doesn’t mean you should clone the entire branch.
  2. 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.

References

Authors
Developer, digital product enthusiast, tinkerer, sharer, open source lover