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 } }
Now let’s set
foo
to 2 for the clone:
> b.foo = 2
2
and look at the values of
a
and
b
now:
> a
{ foo: 1, bar: { a: 2 } }
> b
{ foo: 2, bar: { a: 2 } }
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
Let’s look at the values of
a
and
b
now:
> a
{ foo: 1, bar: { a: 3 } }
> b
{ foo: 2, bar: { a: 3 } }
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 } }
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);
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
.