References and immutability
Value vs reference, immutable updates, shallow vs deep copy.
Updated
What value and reference mean
When you assign a value to a variable, JS does one of two things, depending on the type:
| By value | By reference | |
|---|---|---|
| Types | primitives: string, number, boolean, null, undefined | objects, arrays, functions |
| What gets copied | the value itself | the address of the same value in memory |
| Effect | two independent copies | two names for the same object |
let a = 1
let b = a
b = 2
a // 1 — independent copies
const x = { count: 1 }
const y = x // y points to the same object as x
y.count = 2
x.count // 2 — it's the same object!An analogy: primitives are like a photocopy of a page; objects are like a link to a Google Doc — you send the link, not the document.
Comparison follows the same rule
'a' === 'a' // true — compares values
{ a: 1 } === { a: 1 } // false — two different objects, different addresses
const o = {}; o === o // true — the same addressWhat immutability is
Immutable = you don't change the existing object, you create a new one with the change. The opposite is mutation — changing it in place.
// mutation
user.age = 31
// immutable
const updated = { ...user, age: 31 }Why it matters — especially in React
React decides whether to re-render by comparing the old state with the new one by reference (Object.is). If you change the object in place, the reference is the same → React thinks nothing changed → the UI doesn't update.
// ✗ mutation: the same reference, no re-render
todos.push(newTodo)
setTodos(todos)
// ✓ a new array
setTodos([...todos, newTodo])Other benefits: you can quickly check "did it change?", you get history (undo), and there are no hidden effects between parts of the code that share the same object.
Cheat sheet: immutable updates
| Operation | Array | Object |
|---|---|---|
| add | [...arr, x] |
{ ...obj, key: v } |
| remove | arr.filter(x => x.id !== id) |
const { key, ...rest } = obj |
| change one | arr.map(x => x.id === id ? { ...x, done: true } : x) |
{ ...obj, key: v } |
| sort | arr.toSorted(fn) |
— |
| at an index | arr.with(i, v) |
— |
| nested | — | { ...obj, address: { ...obj.address, city } } |
Shallow copy vs deep copy
| Shallow | Deep | |
|---|---|---|
| How | { ...obj }, [...arr], Object.assign |
structuredClone(obj) |
| What it copies | only the first level; the objects inside stay shared | everything, at any depth |
| Cost | small | bigger |
| When | state updates (you copy only what you change) | when you really need an independent copy |
const copy = { ...user }
copy.address.city = 'Denver' // ✗ also changes user.address — it's the same object
const deep = structuredClone(user)
deep.address.city = 'Denver' // ✓ user stays untouchedJSON.parse(JSON.stringify(x)) — the old hack; it loses Date, Map, undefined, functions. Use structuredClone.
Object.freeze
Object.freeze(obj) blocks changes (first level only). Useful for configuration constants, to catch accidental mutations.
Summary
- Primitives are copied by value, objects by reference.
- Immutable = create a new object instead of changing it; required for state in React.
- Spread copies only the first level;
structuredClonefor a full copy.