Lekce 0.9 · JavaScript pro React
Reference a immutabilita
Hodnota vs. odkaz, mělká kopie, vnořené úpravy bez mutací – a proč na tom React stojí.
Načítám lekci…
Lekce 0.9 · JavaScript pro React
Hodnota vs. odkaz, mělká kopie, vnořené úpravy bez mutací – a proč na tom React stojí.
Načítám lekci…
Předchozí lekce byly obecný JavaScript. Tahle a příští jsou o JavaScriptu, který React vyžaduje. Kdo je přeskočí, potká v Reactu záhadné chyby: „změnil jsem data, ale stránka se nepřekreslila“ nebo „změnil jsem jednu věc a rozbila se jiná“.
Obě chyby mají stejnou příčinu: nepochopení toho, jak JavaScript zachází s objekty a poli. Dvojčata vypadají stejně, ale jsou to dva různí lidé. A jeden člověk může mít dvě přezdívky, ale pořád je to jeden člověk. Přesně v tomhle se JavaScript s objekty liší od čísel a textů.
Primitivní hodnoty (čísla, texty, booleany) se při přiřazení zkopírují. Každá proměnná má svou vlastní:
let a = 5let b = a // b dostane KOPII hodnoty 5b = 10a // 5 – a se nezměnilo
Objekty a pole se chovají jinak. Proměnná v sobě nemá samotný objekt, ale cedulku s adresou, kde objekt bydlí. Přiřazení zkopíruje jen cedulku – obě proměnné pak vedou do stejného domu:
const a = { count: 1 }const b = a // b NENÍ kopie – je to druhá cedulka se STEJNOU adresoub.count = 99a.count // 99 😱 změnil se i „a“ – je to tentýž objektconst list = [1, 2]const sameList = listsameList.push(3)list // [1, 2, 3]
| Primitivní hodnoty | Objekty, pole, funkce | |
|---|---|---|
| Příklady | 5, 'ahoj', true, null | { … }, [ … ], () => … |
| Přiřazení b = a | zkopíruje hodnotu | zkopíruje odkaz (adresu) |
| a === b porovná | hodnotu | jestli je to TENTÝŽ objekt |
| Jdou změnit „uvnitř“? | ne (5 je prostě 5) | ano (vlastnosti, položky) |
'ahoj' === 'ahoj' // true – stejná hodnota[1, 2] === [1, 2] // false – dvě různá pole (dvě adresy), jen vypadají stejně{ a: 1 } === { a: 1 } // falseconst x = { a: 1 }const y = xx === y // true – stejná adresa// Porovnání podle obsahu si musíš napsat sámx.a === y.aJSON.stringify(arr1) === JSON.stringify(arr2) // rychlá (ale nedokonalá) kontrola obsahu
Mutace znamená změnit existující objekt nebo pole „uvnitř“ – přepsat vlastnost, přidat položku, seřadit na místě. Problém nastane, když ten objekt sdílí víc míst v kódu:
function sortByPrice(products) {return products.sort((a, b) => a.price - b.price) // ❌ sort mění předané pole}const catalog = loadProducts() // pořadí podle kataloguconst cheapFirst = sortByPrice(catalog)// 😱 catalog je teď taky seřazený podle ceny – funkce mu ho přeházelafunction sortByPrice(products) {return products.toSorted((a, b) => a.price - b.price) // ✅ vrátí nové pole}
| ❌ Mutuje | ✅ Vyrábí nové |
|---|---|
obj.name = 'x' | { ...obj, name: 'x' } |
delete obj.name | const { name, ...rest } = obj |
arr.push(x) | [...arr, x] |
arr.splice(i, 1) | arr.filter(…) / arr.toSpliced(i, 1) |
arr[i] = x | arr.map(…) / arr.with(i, x) |
arr.sort() | arr.toSorted() |
arr.reverse() | arr.toReversed() |
Co vidíš: Dvě tlačítka, dva způsoby, jak získat pole se švestkou navíc. Pod nimi výpis obou polí.
Vyzkoušej:
fruits a next === fruits je true.next === fruits je false.Co z toho plyne: Přiřazení nekopíruje. Nové pole (nebo objekt) vznikne jen ze spreadu, map, filter a podobných metod – a jen takovou změnu React pozná.
Načítám interaktivní část…
Spread vyrobí mělkou kopii: nový objekt na první úrovni, ale vnořené objekty jsou pořád tytéž. Jako když okopíruješ seznam adres – papír je nový, ale domy na adresách stejné.
const user = { name: 'Jana', address: { city: 'Praha' } }const copy = { ...user }copy === user // false – nový objektcopy.address === user.address // true – vnořený objekt je SDÍLENÝcopy.name = 'Eva' // user.name zůstane 'Jana' ✅copy.address.city = 'Brno' // user.address.city je taky 'Brno' 😱
const deep = structuredClone(user) // zkopíruje všechno do hloubkydeep.address === user.address // false// Starší trik (ztratí Date, funkce, undefined):const deep2 = JSON.parse(JSON.stringify(user))
Tyhle vzory budeš v Reactu psát pořád dokola – při každé změně stavu. Vyplatí se je mít v ruce.
const user = { name: 'Jana', age: 31, address: { city: 'Praha', zip: '11000' } }{ ...user, age: 32 } // změnit vlastnost{ ...user, email: 'jana@example.cz' } // přidat vlastnostconst { age, ...withoutAge } = user // odebrat vlastnost{ ...user, [field]: value } // změnit vlastnost podle proměnné{ ...user, address: { ...user.address, city: 'Brno' } } // změnit vnořenou – spread na každé úrovni{ ...user, age: user.age + 1 } // odvodit z předchozí hodnoty
const todos = [{ id: 1, title: 'Nakoupit', done: false },{ id: 2, title: 'Uklidit', done: true },][...todos, newTodo] // přidat na konec[newTodo, ...todos] // přidat na začátektodos.toSpliced(1, 0, newTodo) // vložit na pozici 1todos.filter((t) => t.id !== id) // odebrat podle idtodos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)) // změnit jednu položkutodos.map((t) => (t.id === id ? { ...t, title: newTitle } : t)) // přejmenovat jednutodos.map((t) => ({ ...t, done: true })) // změnit všechnytodos.filter((t) => !t.done) // odebrat všechny hotovétodos.toSorted((a, b) => a.title.localeCompare(b.title)) // seřadit
const board = {title: 'Projekt',columns: [{ id: 'todo', cards: [{ id: 1, text: 'A' }] },{ id: 'done', cards: [] },],}// Přidat kartu do sloupce 'done'const next = {...board, // 1. nový boardcolumns: board.columns.map((col) => // 2. nové pole sloupcůcol.id === 'done'? { ...col, cards: [...col.cards, newCard] } // 3. nový sloupec s novým polem karet: col, // ostatní sloupce beze změny),}
Postup je pořád stejný: jdi po cestě ke změně a na každé úrovni vyrob kopii; všechno mimo cestu nech, jak je. Když je zanoření moc hluboké, je to signál, že by stav šel uspořádat ploššeji (lekce 5 a 15).
Když v Reactu změníš stav, React porovná starou a novou hodnotu přes === (přesněji Object.is). Když je to tentýž objekt, usoudí, že se nic nezměnilo, a komponentu nepřekreslí.
// Ukázka z lekce 2 – stav s polem úkolůconst [todos, setTodos] = useState([])// ❌ Mutace: pole je pořád TOTÉŽ, React nic nepozná, stránka se nezměnítodos.push(newTodo)setTodos(todos)// ✅ Nové pole: React vidí jinou adresu → překreslísetTodos([...todos, newTodo])
memo, useMemo a závislosti efektů fungují jen díky tomu, že nezměněná data mají stejnou adresu a změněná novou.Přidej položku do pole a přejmenuj uživatele – pokaždé tak, že vznikne nové pole nebo nový objekt.
VÝZVA: Nové místo změněného
Vstupy jsou zmrazené – push ani přiřazení do vlastnosti neprojdou.
1. addFruit(['jablko'], 'hruška') má vrátit NOVÉ pole ['jablko', 'hruška']: [...list, fruit]
2. rename(user, 'Eva') má vrátit NOVÝ objekt se jménem 'Eva': { ...user, name }Načítám interaktivní část…
První výzva procvičí základní neměnné úpravy na jednoduchých datech. Druhá jde do hloubky: stav s projekty a úkoly, kde testy přes === kontrolují, že kopíruješ jen to nutné – přesně jako React.
Šest malých funkcí, které vracejí nová data místo úprav starých. Přesně takové funkce budeš v dalších lekcích psát pro změnu stavu.
VÝZVA: Bez mutací (šipkové funkce, destrukturace, spread, ?. a ??)
Pod tímto komentářem je šest funkcí. Napiš jejich těla. Testy pod nimi se spustí samy
a v kurzu uvidíš u každé ✅ nebo ❌.
Pravidlo: vstupy NIKDY neměň. Vždy vrať nové pole/objekt. Testy vstupy zmrazí
(deepFreeze), takže pokus o mutaci skončí chybou.
1. describe(user) – vrať text „Jana Nováková (Praha)“. Použij destrukturaci a šablonový řetězec `${…}`.
2. addTask(tasks, task) – vrať nové pole s `task` přidaným na konec (spread [...]).
3. removeTask(tasks, id) – vrať nové pole bez úkolu s daným id (filter).
4. toggleTask(tasks, id) – vrať nové pole, kde má úkol s daným id obrácené `done`;
ostatní úkoly zůstanou tytéž objekty (map + spread objektu { ...t, done: … }).
5. moveTo(user, city) – vrať nový objekt uživatele s jiným městem (spread objektu).
6. cityOf(user) – vrať město uživatele, nebo 'neznámé', když uživatel chybí (?. a ??).Načítám interaktivní část…
Stav aplikace s uživatelem, projekty a úkoly. Napiš šest funkcí, které v něm změní jednu věc – a všechno ostatní nechají jako tytéž objekty.
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
=== u objektů porovnává adresu, ne obsah. Dva stejně vypadající objekty nejsou „stejné“.{ ...obj, key: value }.Checks, deepFreeze, equal, Check)Checks, deepFreeze, equal, Check – ze souboru src/course/checks.tsx. Pomocník kurzu pro výzvy s funkcemi: spustí testy a ukáže ✅/❌. Jak funguje uvnitř, nemusíš řešit – stačí, že ti řekne, které kroky už máš hotové./** 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 = falsetests.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) returnsetResults((prev) => prev.map((r, idx) => (idx === i ? result : r)))})})return () => {ignore = true}}, [tests])const passed = results.filter((r) => r.status === 'ok').lengthreturn (<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)}
Checks, deepFreeze, Check)Checks, deepFreeze, Check – ze souboru src/course/checks.tsx. Pomocník kurzu pro výzvy s funkcemi: spustí testy a ukáže ✅/❌. Jak funguje uvnitř, nemusíš řešit – stačí, že ti řekne, které kroky už máš hotové./** 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 = falsetests.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) returnsetResults((prev) => prev.map((r, idx) => (idx === i ? result : r)))})})return () => {ignore = true}}, [tests])const passed = results.filter((r) => r.status === 'ok').lengthreturn (<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}