On this page

Reference vs Value & Copying

3 min read

"Why did my component not re-render?" and "why did my state change by itself?" both come from one idea: JavaScript copies some values and shares others.

Definition

JavaScript has two kinds of values.

KindTypesBehavior on assignment
Primitivesstring, number, boolean, null, undefined, symbol, bigintThe value is copied
Referencesobjects, arrays, functionsThe address is copied, both names point to the same thing

Analogy: a primitive is a photocopy of a page. Change your copy, the original is untouched. A reference is a link to a shared Google Doc. Two people with the link edit the same document.

How It Works

js
1let a = 5;
2let b = a;
3b = 10;
4console.log(a); // 5 ✓ separate copies
5
6const x = { n: 1 };
7const y = x; // copies the address, not the object
8y.n = 99;
9console.log(x.n); // 99 ✗ surprise, same object

Comparison follows the same rule:

js
11 === 1; // true (compares values)
2{} === {}; // false (two different objects)
3[1] === [1]; // false
4const o = {}; o === o; // true (same reference)

Objects are equal only if they are the same object, not if they have the same content.

Shallow Copy

A shallow copy makes a new top-level object, but nested objects are still shared.

js
1const user = { name: "Asha", address: { city: "Pune" } };
2
3const copy = { ...user }; // shallow copy (spread)
4// also: Object.assign({}, user), or [...arr] / arr.slice() for arrays
5
6copy.name = "Ravi"; // ✓ does not affect user
7copy.address.city = "Delhi"; // ✗ also changes user.address.city

Here is what breaks. The top level was copied, but address is one object that both user and copy point to.

Deep Copy

A deep copy duplicates every level.

js
1const deep = structuredClone(user); // ✓ modern, built in
2const old = JSON.parse(JSON.stringify(user)); // old trick, has limits
MethodHandles Date, Map, SetHandles functionsHandles circular refs
structuredCloneYesNo (throws)Yes
JSON.parse(JSON.stringify())No (Date becomes a string)No (dropped)No (throws)
Libraries like lodash cloneDeepYesYesYes

Deep copying large objects is expensive. Usually you only need to copy the path you change, which is what immutable updates do.

Functions and Mutation

Passing an object to a function passes the reference, so the function can change the original.

js
1function rename(u) { u.name = "Ravi"; } // mutates the caller's object

Prefer returning a new object so the caller's data stays safe:

js
1const rename = (u) => ({ ...u, name: "Ravi" });

In React

React decides whether to re-render by comparing the old and new reference of state, not the contents.

jsx
1const [user, setUser] = useState({ name: "Asha" });
2
3user.name = "Ravi"; // ✗ mutated in place
4setUser(user); // same reference → React sees no change → no re-render
5
6setUser({ ...user, name: "Ravi" }); // ✓ new reference → re-render

The same rule drives React.memo, useMemo and dependency arrays. A new {} or [] created in every render has a new reference every time, so it can trigger effects and defeat memoization.

jsx
1useEffect(() => { ... }, [{ a: 1 }]); // ✗ new object each render, effect runs every time

Summary

1Primitives → copied by value
2Objects, arrays, functions → copied by reference (shared)
3=== on objects → same object, not same content
4Shallow copy (spread, slice) → top level only, nested stays shared
5Deep copy → structuredClone for most cases
6Mutating shared data → surprise changes elsewhere
7React → compares references, so always create new objects and arrays for state

The next topic shows the array tools that make "return a new array" easy.

Next TopicImmutable Array Methods