Reference vs Value & Copying
"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.
| Kind | Types | Behavior on assignment |
|---|---|---|
| Primitives | string, number, boolean, null, undefined, symbol, bigint | The value is copied |
| References | objects, arrays, functions | The 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
1let a = 5;2let b = a;3b = 10;4console.log(a); // 5 ✓ separate copies5 6const x = { n: 1 };7const y = x; // copies the address, not the object8y.n = 99;9console.log(x.n); // 99 ✗ surprise, same objectComparison follows the same rule:
11 === 1; // true (compares values)2{} === {}; // false (two different objects)3[1] === [1]; // false4const 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.
1const user = { name: "Asha", address: { city: "Pune" } };2 3const copy = { ...user }; // shallow copy (spread)4// also: Object.assign({}, user), or [...arr] / arr.slice() for arrays5 6copy.name = "Ravi"; // ✓ does not affect user7copy.address.city = "Delhi"; // ✗ also changes user.address.cityHere 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.
1const deep = structuredClone(user); // ✓ modern, built in2const old = JSON.parse(JSON.stringify(user)); // old trick, has limits| Method | Handles Date, Map, Set | Handles functions | Handles circular refs |
|---|---|---|---|
structuredClone | Yes | No (throws) | Yes |
JSON.parse(JSON.stringify()) | No (Date becomes a string) | No (dropped) | No (throws) |
Libraries like lodash cloneDeep | Yes | Yes | Yes |
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.
1function rename(u) { u.name = "Ravi"; } // mutates the caller's objectPrefer returning a new object so the caller's data stays safe:
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.
1const [user, setUser] = useState({ name: "Asha" });2 3user.name = "Ravi"; // ✗ mutated in place4setUser(user); // same reference → React sees no change → no re-render5 6setUser({ ...user, name: "Ravi" }); // ✓ new reference → re-renderThe 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.
1useEffect(() => { ... }, [{ a: 1 }]); // ✗ new object each render, effect runs every timeSummary
1Primitives → copied by value2Objects, arrays, functions → copied by reference (shared)3=== on objects → same object, not same content4Shallow copy (spread, slice) → top level only, nested stays shared5Deep copy → structuredClone for most cases6Mutating shared data → surprise changes elsewhere7React → compares references, so always create new objects and arrays for stateThe next topic shows the array tools that make "return a new array" easy.