Skip to content
React Course1.0.0 beta
CSEN
React Course1.0.0 beta
CSEN
🏠 Home🧭 Where to start?🔁 Review📄 Cheat sheets📰 What's newℹ️ About the course
0. JavaScript0/22

JavaScript in general

  • 0.1 Variables, types and operators0/2
  • 0.2 Functions and closures0/2
  • 0.3 Objects0/2
  • 0.4 Arrays I: map, filter and friends0/2
  • 0.5 Arrays II: reduce, sorting and grouping0/3
  • 0.6 Conditions and logical operators0/2
  • 0.7 Asynchronous JavaScript0/2
  • 0.8 Modules and built-in tools0/2

JavaScript for React

  • 0.9 References and immutability0/3
  • 0.10 JS patterns you meet everywhere in React0/2

Basics

  • 1. A quick review of the basics0/3
  • 2. State and events (useState)0/3

Hooks in depth

  • 3. Effects (useEffect)0/3
  • 4. Refs (useRef)0/3
  • 5. More complex state (useReducer)0/3
  • 6. Context0/3
  • 7. Custom hooks0/3

Forms

  • 8. Forms and React 19 Actions0/3

Performance

  • 9. Performance and rendering0/3

Patterns

  • 10. Component design patterns0/3

Ecosystem

  • 11. Where the type goes (TypeScript syntax)0/3
  • 12. TypeScript with React0/3
  • 13. Routing (React Router)0/3
  • 14. Data fetching0/3
  • 15. Application state management0/3

Project

  • 16. Final project: Kanban0/2

Summary

  • 17. Summary: the principles of React

Your account

Privacy

Lesson 0.9 · JavaScript for React

References and immutability

Value vs. reference, shallow copies, nested updates without mutation – and why React depends on it.

Loading lesson…

💬 Found a mistake, something unclear, or have an idea? Let me know.
← Modules and built-in toolsJS patterns you meet everywhere in React →

Before we start: why this lesson belongs to React

The previous lessons were general JavaScript. This one and the next are about the JavaScript that React requires. Skip them and you'll meet mysterious bugs in React: "I changed the data but the page didn't re-render" or "I changed one thing and something else broke".

Both bugs have the same cause: not understanding how JavaScript treats objects and arrays. Twins look the same, but they're two different people. And one person can have two nicknames, but it's still one person. This is exactly where JavaScript treats objects differently from numbers and text.

ℹ️ What's in this lesson
  1. Value vs. reference: a tag with an address.
  2. Comparing objects with ===.
  3. Mutation: when a function changes what isn't its own.
  4. Shallow and deep copies.
  5. A cookbook of immutable updates for objects and arrays.
  6. Why React depends on it.

1. Value vs. reference

Primitive values (numbers, text, booleans) are copied when assigned. Each variable has its own:

let a = 5
let b = a // b gets a COPY of the value 5
b = 10
a // 5 – a didn't change

Objects and arrays behave differently. The variable doesn't hold the object itself, but a tag with the address where the object lives. Assignment copies just the tag – both variables then lead to the same house:

const a = { count: 1 }
const b = a // b is NOT a copy – it's a second tag with THE SAME address
b.count = 99
a.count // 99 😱 "a" changed too – it's the same object
const list = [1, 2]
const sameList = list
sameList.push(3)
list // [1, 2, 3]
Primitive valuesObjects, arrays, functions
Examples5, 'hello', true, null{ … }, [ … ], () => …
Assignment b = acopies the valuecopies the reference (the address)
a === b comparesthe valuewhether it’s THE SAME object
Can they be changed “inside”?no (5 is simply 5)yes (properties, items)

2. Comparing objects

'hello' === 'hello' // true – the same value
[1, 2] === [1, 2] // false – two different arrays (two addresses) that just look the same
{ a: 1 } === { a: 1 } // false
const x = { a: 1 }
const y = x
x === y // true – the same address
// Comparing by content is up to you
x.a === y.a
JSON.stringify(arr1) === JSON.stringify(arr2) // a quick (but imperfect) content check
❓ Why doesn't JavaScript compare the content?

Because it would be slow: for a big nested object it would have to go through every property. Comparing addresses is instant. And React takes advantage of exactly this speed – it doesn't have to go through the whole state, it only needs to find out whether it got a new object.

3. Mutation: when a function changes what isn't its own

Mutation means changing an existing object or array "inside" – overwriting a property, adding an item, sorting in place. The trouble starts when several places in the code share that object:

function sortByPrice(products) {
return products.sort((a, b) => a.price - b.price) // ❌ sort changes the array it was given
}
const catalog = loadProducts() // in catalogue order
const cheapFirst = sortByPrice(catalog)
// 😱 catalog is now sorted by price too – the function reshuffled it
function sortByPrice(products) {
return products.toSorted((a, b) => a.price - b.price) // ✅ returns a new array
}
❌ Mutates✅ Makes something new
obj.name = 'x'{ ...obj, name: 'x' }
delete obj.nameconst { name, ...rest } = obj
arr.push(x)[...arr, x]
arr.splice(i, 1)arr.filter(…) / arr.toSpliced(i, 1)
arr[i] = xarr.map(…) / arr.with(i, x)
arr.sort()arr.toSorted()
arr.reverse()arr.toReversed()
Demo

Push vs. spread

What you see: Two buttons, two ways to get an array with an extra plum. Below them, a printout of both arrays.

Try it:

  1. Click Copy the variable and push. The plum was added to the original fruits too, and next === fruits is true.
  2. Click A new array with spread. The original array stayed, next === fruits is false.

The takeaway: Assignment doesn't copy. A new array (or object) is only created by spread, map, filter and similar methods – and that's the only kind of change React notices.

Loading the interactive part…

4. Shallow and deep copies

Spread makes a shallow copy: a new object on the first level, but the nested objects are still the same. Like photocopying a list of addresses – the paper is new, but the houses at the addresses are the same.

const user = { name: 'Anna', address: { city: 'London' } }
const copy = { ...user }
copy === user // false – a new object
copy.address === user.address // true – the nested object is SHARED
copy.name = 'Eve' // user.name stays 'Anna' ✅
copy.address.city = 'Boston' // user.address.city is 'Boston' too 😱

Deep copy

const deep = structuredClone(user) // copies everything all the way down
deep.address === user.address // false
// The older trick (loses Date, functions, undefined):
const deep2 = JSON.parse(JSON.stringify(user))
✨ In React you usually don't want a deep copy
Copying the whole state on every change is unnecessary and slow. And it breaks optimisations too: React (and memo) would think absolutely everything had changed. The right way is to copy only what lies on the path to the change – see the cookbook below.

5. A cookbook of immutable updates

You'll write these patterns in React again and again – on every state change. They're worth having at your fingertips.

Objects

const user = { name: 'Anna', age: 31, address: { city: 'London', zip: 'SW1A 1AA' } }
{ ...user, age: 32 } // change a property
{ ...user, email: 'anna@example.com' } // add a property
const { age, ...withoutAge } = user // remove a property
{ ...user, [field]: value } // change a property named by a variable
{ ...user, address: { ...user.address, city: 'Boston' } } // change a nested one – spread on every level
{ ...user, age: user.age + 1 } // derive from the previous value

Arrays

const todos = [
{ id: 1, title: 'Go shopping', done: false },
{ id: 2, title: 'Tidy up', done: true },
]
[...todos, newTodo] // add to the end
[newTodo, ...todos] // add to the start
todos.toSpliced(1, 0, newTodo) // insert at position 1
todos.filter((t) => t.id !== id) // remove by id
todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)) // change one item
todos.map((t) => (t.id === id ? { ...t, title: newTitle } : t)) // rename one
todos.map((t) => ({ ...t, done: true })) // change all of them
todos.filter((t) => !t.done) // remove all the finished ones
todos.toSorted((a, b) => a.title.localeCompare(b.title)) // sort
⚠️ map: return the unchanged items as they are
todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)) // ✅ the others are THE SAME objects
todos.map((t) => (t.id === id ? { ...t, done: !t.done } : { ...t })) // ⚠️ needlessly copies all of them
todos.map((t) => { if (t.id === id) t.done = !t.done; return t }) // ❌ mutates the original object!

An array inside an object (and the other way round)

const board = {
title: 'Project',
columns: [
{ id: 'todo', cards: [{ id: 1, text: 'A' }] },
{ id: 'done', cards: [] },
],
}
// Add a card to the 'done' column
const next = {
...board, // 1. a new board
columns: board.columns.map((col) => // 2. a new array of columns
col.id === 'done'
? { ...col, cards: [...col.cards, newCard] } // 3. a new column with a new array of cards
: col, // the other columns unchanged
),
}

The procedure is always the same: follow the path to the change and make a copy on every level; leave everything off the path as it is. When the nesting gets too deep, it's a sign the state could be organised more flatly (lessons 5 and 15).

6. Why React depends on it

When you change state in React, React compares the old and the new value with === (more precisely Object.is). When it's the same object, it concludes nothing has changed and doesn't re-render the component.

// An example from lesson 2 – state with an array of todos
const [todos, setTodos] = useState([])
// ❌ Mutation: the array is still THE SAME, React notices nothing, the page doesn't change
todos.push(newTodo)
setTodos(todos)
// ✅ A new array: React sees a different address → re-renders
setTodos([...todos, newTodo])
  • Fast change detection: comparing the address is enough, no need to go through the content.
  • Optimisations: memo, useMemo and effect dependencies only work because unchanged data keep the same address and changed data get a new one.
  • Predictability: every render works with its own "snapshot" of the data that nobody changes behind its back.
  • Undo/redo for free: old versions of the state stay untouched – you just have to keep them (lesson 5).
✨ Object.freeze – a safety net while learning
Object.freeze(obj) forbids changes to an object. In strict mode (modules always have it) every attempt at a mutation then throws an error. That's how the course's challenges make sure you don't change anything. Careful, freeze is shallow too – you have to freeze nested objects separately.

Check yourself

Quiz const b = a; b.push(3) – what happens to the array a?
Quiz What does { a: 1 } === { a: 1 } return?
Quiz How do you correctly change done of the todo with id 2 in the todos array?
Quiz const copy = { ...user }; copy.address.city = 'Boston'. Does user.address.city change?

Warm-up

Challenge

New instead of changed

Warm-up

Add an item to an array and rename a user – each time so that a new array or a new object is created.

Task (the same text is in the comment at the top of the file)
CHALLENGE: New instead of changed

The inputs are frozen – neither push nor assigning to a property will work.

1. addFruit(['apple'], 'pear') should return a NEW array ['apple', 'pear']: [...list, fruit]
2. rename(user, 'Eve') should return a NEW object with the name 'Eve': { ...user, name }

Loading the interactive part…

Challenges

The first challenge practises the basic immutable updates on simple data. The second goes deeper: state with projects and tasks, where the tests use === to check that you copy only what's necessary – exactly like React.

Challenge

No mutations

Easy

Six small functions that return new data instead of changing the old. You'll write exactly such functions for changing state in the next lessons.

Task (the same text is in the comment at the top of the file)
CHALLENGE: No mutations (arrow functions, destructuring, spread, ?. and ??)

Below this comment there are six functions. Write their bodies. The tests below them run on their own
and in the course you'll see ✅ or ❌ next to each one.

The rule: NEVER change the inputs. Always return a new array/object. The tests freeze the inputs
(deepFreeze), so an attempt at a mutation ends with an error.

1. describe(user) – return the text 'Anna Smith (London)'. Use destructuring and a template string `${…}`.
2. addTask(tasks, task) – return a new array with `task` added to the end (spread [...]).
3. removeTask(tasks, id) – return a new array without the task with the given id (filter).
4. toggleTask(tasks, id) – return a new array where the task with the given id has `done` flipped;
   the other tasks stay the same objects (map + object spread { ...t, done: … }).
5. moveTo(user, city) – return a new user object with a different city (object spread).
6. cityOf(user) – return the user's city, or 'unknown' when the user is missing (?. and ??).

Loading the interactive part…

Challenge

Nested updates

Hard Bonus challenge

The app state with a user, projects and tasks. Write six functions that change one thing in it – and leave everything else as the same objects.

🔒 Bonus challenge

Medium and hard challenges are a bonus for signed-in readers. Signing up is free – just an e-mail, no password.

How we handle your data

Summary

  • Primitive values are copied, objects and arrays are shared. A variable holding an object is a tag with an address.
  • === on objects compares the address, not the content. Two objects that look the same aren't "the same".
  • Mutation changes the object for everyone who refers to it. push, splice, sort and assigning to a property mutate.
  • Spread is shallow. With nested data, copy every level on the path to the change and leave the rest.
  • The cookbook: add = spread, remove = filter, change one item = map with a ternary, change a property = { ...obj, key: value }.
  • React notices a change only by a new address. Mutated state = a page that doesn't re-render.
🧩 Where do the things not defined here come from (Checks, deepFreeze, equal, Check)
Checks, deepFreeze, equal, Check – from the file src/course/checks.tsx. The course helper for function-style challenges: runs the tests and shows ✅/❌. You don't need to know how it works inside – it just tells you which steps are done.
/** One test: a description and a function returning true (passed) or false. It may be async. */
export type Check = [name: string, test: () => boolean | Promise<boolean>]
/**
* Runs the tests of a challenge and shows ✅ or ❌ for each.
* An error thrown in a test (e.g. when trying to change a frozen object) is shown under the test.
*/
export function Checks({ tests }: { tests: Check[] }) {
const t = useT()
const [results, setResults] = useState<Result[]>(() => tests.map(() => ({ status: 'pending' })))
useEffect(() => {
let ignore = false
tests.forEach(([, test], i) => {
Promise.resolve()
.then(test)
.then(
(ok): Result => (ok ? { status: 'ok' } : { status: 'fail' }),
(e): Result => ({ status: 'fail', error: e instanceof Error ? e.message : String(e) }),
)
.then((result) => {
if (ignore) return
setResults((prev) => prev.map((r, idx) => (idx === i ? result : r)))
})
})
return () => {
ignore = true
}
}, [tests])
const passed = results.filter((r) => r.status === 'ok').length
return (
<div className="stack">
<strong className={passed === tests.length ? 'ok' : undefined}>
{t.checksPassed(passed, tests.length)}
</strong>
<ul className="list">
{tests.map(([name], i) => {
const r = results[i] ?? { status: 'pending' }
return (
<li key={name}>
{r.status === 'pending' ? '⏳' : r.status === 'ok' ? '✅' : '❌'} {name}
{r.status === 'fail' && r.error && (
<div className="bad muted">
{t.checksError} {r.error}
</div>
)}
</li>
)
})}
</ul>
</div>
)
}
/** Freezes an object and all nested objects and arrays – any attempt to change them throws. */
export function deepFreeze<T>(value: T): T {
if (value && typeof value === 'object') {
Object.values(value).forEach(deepFreeze)
Object.freeze(value)
}
return value
}
/** Compares two values by content (arrays and objects item by item), not by reference. */
export function equal(a: unknown, b: unknown): boolean {
return JSON.stringify(a) === JSON.stringify(b)
}
🧩 Where do the things not defined here come from (Checks, deepFreeze, Check)
Checks, deepFreeze, Check – from the file src/course/checks.tsx. The course helper for function-style challenges: runs the tests and shows ✅/❌. You don't need to know how it works inside – it just tells you which steps are done.
/** One test: a description and a function returning true (passed) or false. It may be async. */
export type Check = [name: string, test: () => boolean | Promise<boolean>]
/**
* Runs the tests of a challenge and shows ✅ or ❌ for each.
* An error thrown in a test (e.g. when trying to change a frozen object) is shown under the test.
*/
export function Checks({ tests }: { tests: Check[] }) {
const t = useT()
const [results, setResults] = useState<Result[]>(() => tests.map(() => ({ status: 'pending' })))
useEffect(() => {
let ignore = false
tests.forEach(([, test], i) => {
Promise.resolve()
.then(test)
.then(
(ok): Result => (ok ? { status: 'ok' } : { status: 'fail' }),
(e): Result => ({ status: 'fail', error: e instanceof Error ? e.message : String(e) }),
)
.then((result) => {
if (ignore) return
setResults((prev) => prev.map((r, idx) => (idx === i ? result : r)))
})
})
return () => {
ignore = true
}
}, [tests])
const passed = results.filter((r) => r.status === 'ok').length
return (
<div className="stack">
<strong className={passed === tests.length ? 'ok' : undefined}>
{t.checksPassed(passed, tests.length)}
</strong>
<ul className="list">
{tests.map(([name], i) => {
const r = results[i] ?? { status: 'pending' }
return (
<li key={name}>
{r.status === 'pending' ? '⏳' : r.status === 'ok' ? '✅' : '❌'} {name}
{r.status === 'fail' && r.error && (
<div className="bad muted">
{t.checksError} {r.error}
</div>
)}
</li>
)
})}
</ul>
</div>
)
}
/** Freezes an object and all nested objects and arrays – any attempt to change them throws. */
export function deepFreeze<T>(value: T): T {
if (value && typeof value === 'object') {
Object.values(value).forEach(deepFreeze)
Object.freeze(value)
}
return value
}