DEV Community

CP
CP

Posted on

3 2

Beware of Mutation in Spread & React Hooks and How Can You Fix It

Shallow copy

We can use spread operator to clone objects. But beware that it only does a shallow clone: you get a new copy of the top level variables, but the nested objects are still pointing to the same pointer.

This shallow copy may cause unexpected mutations. Let's illustrate that in an example.

Understand with an example

Let's create a nested object a, and clone it to b using spread operator:

> a = { foo: 1, bar: { a: 2 } }
{ foo: 1, bar: { a: 2 } }
> b = { ...a }
{ foo: 1, bar: { a: 2 } }
Enter fullscreen mode Exit fullscreen mode

Now let's set foo to 2 for the clone:

> b.foo = 2
2
Enter fullscreen mode Exit fullscreen mode

and look at the values of a and b now:

> a
{ foo: 1, bar: { a: 2 } }
> b
{ foo: 2, bar: { a: 2 } }
Enter fullscreen mode Exit fullscreen mode

See b.foo is updated to 2, while a.foo remains "untouched".

Now, what if we update the value for foo.bar.a?

> b.bar.a = 3
3
Enter fullscreen mode Exit fullscreen mode

Let's look at the values of a and b now:

> a
{ foo: 1, bar: { a: 3 } }
> b
{ foo: 2, bar: { a: 3 } }
Enter fullscreen mode Exit fullscreen mode

Wow, how come a.bar.a value has changed???
That is because in object b, the b.bar is sharing the same pointer of a.bar, so these two values will change together, surprising mutation if we were not aware of this.

React Hooks

The shallow copy happens to the React Hooks useState, too.

Using the example above:

a = { foo: 1, bar: { a: 2 } };

const [obj, setObj] = useState(a);

const b = { ...obj };

b.foo = 2;
b.bar.a = 3;
setObj(b);

// You will get:
// obj = { foo: 2, bar: { a: 3 } }
// and a will be mutated to:
// a = { foo: 1, bar: { a: 3 } }
Enter fullscreen mode Exit fullscreen mode

React Hooks only do a shallow copy, so beware of the mutation.

Fix with Lodash

There is a simple fix: use lodash cloneDeep:

import { cloneDeep } from "lodash";

a = { foo: 1, bar: { a: 2 } };

const [obj, setObj] = useState(cloneDeep(a));

// or instead of spread operator
//   const b = { ...a };
// call cloneDeep instead:
//   const b = cloneDeep(a);
Enter fullscreen mode Exit fullscreen mode

Unexpected mutation could cause unexpected data pollution and very hard to debug. Using lodash cloneDeep will ensure there is no "cross-contamination" of your variables, and it is simpler than setting up Immutable.

SurveyJS custom survey software

Simplify data collection in your JS app with a fully integrated form management platform. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more. Integrates with any backend system, giving you full control over your data and no user limits.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs