Přeskočit na obsah
Kurz Reactu1.0.0 beta
CSEN
Kurz Reactu1.0.0 beta
CSEN
🏠 Úvod🧭 Kde začít?🔁 Opakování📄 Taháky📰 Co je novéhoℹ️ O kurzu
JavaScript0/22
TypeScript0/6

Základy

  • 01 Rychlé opakování základů0/3
  • 02 Stav a události (useState)0/3

Hooky do hloubky

  • 03 Efekty (useEffect)0/3
  • 04 Refy (useRef)0/3
  • 05 Složitější stav (useReducer)0/3
  • 06 Context0/3
  • 07 Vlastní hooky0/3

Formuláře

  • 08 Formuláře a React 19 Actions0/3

Výkon

  • 09 Výkon a renderování0/3

Vzory

  • 10 Návrhové vzory komponent0/3

Ekosystém

  • 11 TypeScript s Reactem0/3
  • 12 Routování (React Router)0/3
  • 13 Načítání dat0/3
  • 14 Správa stavu aplikace0/3

Projekt

  • 15 Závěrečný projekt: Kanban0/2

Shrnutí

  • 16 Shrnutí: principy Reactu

Tvůj účet

Ochrana osobních údajů

React 13 · Ekosystém

Načítání dat

Proč fetch v useEffect nestačí, TanStack Query, Suspense a use().

Načítám lekci…

💬 Našel jsi chybu, něco ti není jasné, nebo máš nápad? Napiš mi.
← Routování (React Router)Správa stavu aplikace →

Než začneme: opsaný ceník

Představ si kancelář, kde pět lidí potřebuje znát aktuální ceny od dodavatele. Každý si zavolá sám a opíše si ceník do svého bloku. Dodavatel dostane pět stejných telefonátů. Za týden dodavatel ceny změní – a v kanceláři je pět bloků se starými cenami, o kterých nikdo neví, že jsou staré.

Tohle je podstata dat ze serveru: nepatří ti. Máš jen jejich opis, který může kdykoli zastarat. Lokální stav (otevřené menu, text v poli) je tvůj sešit – víš přesně, co v něm je. Serverová data jsou opsaný ceník. Potřebují tedy něco, co lokální stav nezná:

  • Jeden opis pro všechny – když se ptá pět komponent, zavolá se jednou (deduplikace) a výsledek se sdílí (cache).
  • Vědět, kdy je opis starý – a obnovit ho (po čase, po návratu do okna, po změně).
  • Přeškrtnout opis po změně – když sám něco změníš u dodavatele, starý opis už neplatí.
  • Řešit závody odpovědí, opakování při chybě, stránkování…

Lepší kancelář má proto nástěnku se správcem. Kdo potřebuje ceník, podívá se na nástěnku. Správce dodavateli volá jen jednou, ví, jak starý je každý údaj, a když je potřeba, zavolá znovu. Takovým správcem je v Reactu knihovna TanStack Query.

ℹ️ Co tě v lekci čeká
  1. Proč ruční načítání v efektu nestačí.
  2. useQuery: správce nástěnky.
  3. useMutation a invalidace: změny na serveru.
  4. Suspense a use(): načítání popsané deklarativně.

1. Proč fetch v efektu nestačí

Z lekce o efektech umíš načíst uživatele takhle:

function UserName({ id }: { id: number }) {
const [user, setUser] = useState<User | null>(null)
useEffect(() => {
let ignore = false
fetchUser(id).then((u) => !ignore && setUser(u))
return () => { ignore = true }
}, [id])
return <span>{user ? user.name : 'Načítám…'}</span>
}

Funguje to, ale každá komponenta je ten člověk s vlastním blokem:

  • Když je na stránce UserName třikrát se stejným id, odejdou tři stejné požadavky.
  • Když komponentu skryješ a znovu ukážeš, opis se zahodil – zase „Načítám…“ a další požadavek.
  • Když se data na serveru změní, nikdo se to nedozví.
  • A to jsme ještě neřešili chyby, opakování ani stránkování.
Ukázka

useEffect vs. useQuery

Co vidíš: Vlevo tři komponenty, které načítají stejného uživatele přes useEffect. Vpravo tři, které ho načítají přes useQuery. Dole počítadla skutečných požadavků na server.

Vyzkoušej:

  1. Po načtení klikni na Obnovit počítadla. Vlevo bude 6 (tři komponenty a ve vývojovém režimu běží každý efekt dvakrát), vpravo 1 – správce zavolal jednou a výsledek rozdal všem.
  2. Klikni na Odpojit komponenty a pak na Znovu připojit. Vlevo se znovu ukáže „Načítám…“, vpravo jsou data hned – vzala se z nástěnky.
  3. Obnov počítadla. Vlevo přibyly další požadavky, vpravo žádný (data jsou ještě 10 s považovaná za čerstvá).

Co z toho plyne: Ruční načítání nemá společnou paměť. useQuery sdílí jeden opis, slučuje stejné požadavky a drží data i pro komponenty, které se teprve objeví.

Načítám interaktivní část…

2. useQuery: správce nástěnky

// main.tsx – jednou pro celou aplikaci: zřídíme nástěnku
const queryClient = new QueryClient()
<QueryClientProvider client={queryClient}><App /></QueryClientProvider>
// kdekoli v aplikaci
function UserName({ id }: { id: number }) {
const { data, isPending, isError, error } = useQuery({
queryKey: ['user', id], // štítek: co přesně chci
queryFn: () => fetchUser(id), // jak to získat, když to na nástěnce není
staleTime: 60_000, // minutu je údaj čerstvý
})
if (isPending) return <span>Načítám…</span>
if (isError) return <span>{error.message}</span>
return <span>{data.name}</span>
}

Co se děje, když se na stránce objeví tři UserName s id = 2:

  1. První se zeptá správce na štítek ['user', 2]. Na nástěnce nic není → správce zavolá queryFn. Komponenta je ve stavu isPending.
  2. Druhá a třetí se zeptají na stejný štítek. Správce ví, že už volá – nový požadavek neposílá, jen si je zapíše.
  3. Odpověď přijde → správce ji vyvěsí na nástěnku a všem třem dá vědět. Všechny tři ukážou jméno.
  4. Za pět minut se objeví čtvrtá. Údaj je na nástěnce, takže ho dostane okamžitě. Protože je starší než staleTime, správce ho zároveň na pozadí obnoví.
❓ Co znamená const { data: user } = useQuery(…)?

Destrukturace s přejmenováním: „vezmi vlastnost data a ulož ji do proměnné user“. useQuery vrací data vždy pod jménem data. Když máš v komponentě dva dotazy, oba by se jmenovaly data, proto si je přejmenuješ:

const { data: user } = useQuery({ queryKey: ['user', id], … })
const { data: posts } = useQuery({ queryKey: ['posts', id], … })

Je to ta dvojtečka v destrukturaci z lekce TypeScript 01 (kapitola „2. Kde se dvojtečka píše a kde ne“): tady neznamená typ, ale nové jméno.

queryKey: štítek na šanonu

Podle štítku správce hledá data na nástěnce. Musí proto obsahovat všechno, na čem dotaz závisí. Ceník ovoce, strana 2 je jiný šanon než ceník zeleniny, strana 1:

useQuery({
queryKey: ['products', { category, page }], // ✅ změna kategorie nebo stránky = jiný šanon
queryFn: () => fetchProducts({ category, page }),
})
useQuery({
queryKey: ['products'], // ❌ všechny stránky by sdílely jeden šanon
queryFn: () => fetchProducts({ category, page }),
})
PojemVýznam
queryKeyŠtítek na šanonu. Obsahuje vše, na čem queryFn závisí.
staleTimeJak dlouho je údaj čerstvý (výchozí 0 → obnoví se při každém připojení nebo návratu do okna).
gcTimeJak dlouho drží nástěnka data, která nikdo nepoužívá (výchozí 5 min).
isPendingJeště nemáme žádná data (první načtení).
isFetchingPrávě běží požadavek – i na pozadí, když už data máme.
placeholderDataCo ukázat, než přijdou data – např. keepPreviousData při stránkování.

3. Mutace a invalidace: změny na serveru

Když sám zavoláš dodavateli a objednáš změnu, víš, že opis ceníku na nástěnce už neplatí. Přeškrtneš ho, a správce sežene nový. Přesně tohle dělá dvojice useMutation + invalidateQueries:

function NewPost() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createPost, // změna na serveru
onSuccess: () => {
// přeškrtni všechny šanony začínající 'posts'
return queryClient.invalidateQueries({ queryKey: ['posts'] })
},
})
return (
<button onClick={() => mutation.mutate('Nový příspěvek')} disabled={mutation.isPending}>
{mutation.isPending ? 'Ukládám…' : 'Přidat'}
</button>
)
}
  1. mutation.mutate(…) zavolá createPost. Během čekání je isPending true.
  2. Po úspěchu invalidateQueries označí šanony ['posts', …] za zastaralé – všechny stránky seznamu najednou.
  3. Ty, které jsou právě na obrazovce, správce hned načte znovu. Ostatní až ve chvíli, kdy je někdo bude potřebovat.
Ukázka

Stránkování + přidání příspěvku

Co vidíš: Nahoře formulář na nový příspěvek (mutace), dole stránkovaný seznam (dotaz s ['posts', { page }]). Při obnovování dat se ukáže „🔄 aktualizuji…“.

Vyzkoušej:

  1. Klikni na Další →. Stará stránka zůstane vidět (zprůhledněná), dokud nepřijde nová – díky keepPreviousData nic neblikne.
  2. Vrať se na první stránku. Je tam okamžitě – šanon už na nástěnce visí.
  3. Přidej příspěvek. Po uložení se seznam sám obnoví a nový příspěvek se objeví – mutace přeškrtla šanony posts.
  4. Zkus přidat příspěvek se slovem „chyba“. Server ho odmítne a pod formulářem se ukáže hláška.

Co z toho plyne: Dotazy čtou z nástěnky, mutace mění server a přeškrtnou staré opisy. O zbytek se postará správce.

Načítám interaktivní část…

📍 Kde to použít
TanStack Query (nebo SWR, RTK Query, Apollo pro GraphQL) použij v každé aplikaci, která mluví s API. Je to nejlepší jednotlivé zlepšení, které můžeš typické React aplikaci dát – a zmizí s ním většina „globálního stavu“.

4. Suspense a use(): čekárna s cedulí

V restauraci může každý stůl hlásit „ještě nemám polévku“, „ještě nemám hlavní jídlo“… Nebo celá sekce dostane ceduli „připravujeme, chvilku strpení“ a jídla se donesou, až jsou hotová. Druhý způsob je přehlednější – číšník řeší čekání na jednom místě.

Místo if (isPending) v každé komponentě můžeš čekání popsat jednou, o úroveň výš: obalit část stromu do <Suspense fallback>. Komponenty uvnitř pak data prostě „mají“.

function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise) // tady už máme data – žádné isPending, žádné undefined
return <h2>{user.name}</h2>
}
<ErrorBoundary fallback={<p>Uživatel nenalezen</p>}>
<Suspense fallback={<p>Připravujeme…</p>}>
<UserProfile userPromise={getUser(id)} />
</Suspense>
</ErrorBoundary>
  1. use(userPromise) se podívá, jestli je Promise hotový. Pokud ne, komponenta řekne „ještě čekám“ a React ukáže nejbližší fallback.
  2. Až se Promise vyřeší, React komponentu vykreslí znovu – a use tentokrát vrátí data.
  3. Když Promise selže, chybu chytí nejbližší error boundary z minulé lekce.
Ukázka

use(promise) + Suspense + Error Boundary

Co vidíš: Tlačítka #1–#5 a profil vybraného uživatele. Profil používá use(promise), čekání řeší Suspense, chyby error boundary. Přepínání je obalené v startTransition.

Vyzkoušej:

  1. Při prvním načtení uvidíš „⏳ Suspense fallback…“ – čekárna s cedulí.
  2. Klikni na #2. Starý profil zůstane vidět (zprůhledněný), dokud nepřijde nový – transition z lekce o výkonu drží starý obsah místo cedule.
  3. Vrať se na #1. Je tam hned – Promise je uložený v cache.
  4. Klikni na #5. Ten neexistuje, Promise selže a místo profilu se ukáže chyba z error boundary.

Co z toho plyne: Komponenta s daty neřeší čekání ani chyby. Čekání popisuje Suspense, chyby error boundary – o úroveň výš a na jednom místě.

Načítám interaktivní část…

ZpůsobKdy
useQueryVýchozí volba. Čekání a chyby řešíš v komponentě.
useSuspenseQueryTotéž s TanStack Query, ale čekání řeší Suspense a chyby error boundary. data nikdy není undefined.
use(promise)Promise přichází zvenku (loader routeru, Server Component, vlastní cache).
lazy(() => import())Líné načtení kódu komponenty – také přes Suspense.
⚠️ Pozor
use(fetchUser(id)) přímo v komponentě nefunguje – každý render vytvoří nový Promise, komponenta čeká na nový a nový a zacyklí se. Promise musí být uložený mimo render (cache, loader, knihovna).
✨ Vodopády
V restauraci si můžeš nejdřív objednat pití, počkat, až ho přinesou, a teprve pak si objednat jídlo. Pomalé. Stejně pomalé je, když rodič čeká na svá data a teprve pak vykreslí dítě, které začne načítat svoje. Řešení: objednej všechno najednou a co nejdřív – loader routeru, prefetchQuery, useQueries.

Ověř si to

Kvíz Uživatel přidá úkol přes useMutation. Jak zajistit, že se seznam úkolů aktualizuje?
Kvíz Co patří do queryKey pro dotaz fetchProducts({ category, page })?
Kvíz Tři komponenty volají useQuery se stejným queryKey ve stejnou chvíli. Kolik požadavků odejde na server?

Rozcvička

Výzva

První useQuery

Rozcvička

Načti seznam produktů přes TanStack Query. Načítání i seznam jsou hotové.

Zadání najdeš v komentářích v kódu níže – na začátku souboru a u míst označených TODO.

Načítám interaktivní část…

Výzvy

První výzva je přechod z kapitoly 1 do kapitoly 2: přepiš načítání z efektu na useQuery a přidej přednačtení. Druhá rozšíří kapitolu 3 o optimistickou změnu přímo na nástěnce – se zálohou pro případ, že server změnu odmítne.

❓ Co jsou queryOptions a prefetchQuery (výzva Z useEffect na useQuery)?

prefetchQuery řekne správci „začni tohle shánět už teď, i když to zatím nikdo nezobrazuje“. Typicky při najetí myší na odkaz: než uživatel klikne, data už jsou na nástěnce.

Aby useQuery i prefetchQuery mluvily o stejném šanonu, musí dostat stejný queryKey a queryFn. queryOptions je pomocník, který tyhle volby zabalí do jednoho objektu. Napíšeš je jednou a použiješ všude:

const userQuery = (id: number) =>
queryOptions({ queryKey: ['user', id], queryFn: () => fetchUser(id), staleTime: 30_000 })
useQuery(userQuery(id)) // v komponentě
queryClient.prefetchQuery(userQuery(id)) // při najetí myší
queryClient.invalidateQueries({ queryKey: userQuery(id).queryKey })
❓ Jak funguje optimistická změna v cache (výzva Optimistické lajky)?

Princip je stejný jako u useOptimistic v lekci o formulářích: ukaž změnu hned, a když server selže, vrať ji. Jen se neupravuje stav komponenty, ale opis na nástěnce. useMutation k tomu nabízí tři „háčky“ v čase:

useMutation({
mutationFn: (id: number) => likePost(id),
// 1) PŘED odesláním: zastav rozběhnuté načítání, zálohuj a uprav nástěnku
onMutate: async (id) => {
await queryClient.cancelQueries({ queryKey }) // ať nám ho nepřepíše starý refetch
const previous = queryClient.getQueryData<PostsPage>(queryKey) // záloha
queryClient.setQueryData<PostsPage>(queryKey, (old) => …) // +1 lajk hned
return { previous } // → dostane onError jako context
},
// 2) PŘI CHYBĚ: vrať zálohu
onError: (err, id, context) => {
queryClient.setQueryData(queryKey, context?.previous)
},
// 3) VŽDY NAKONEC: přeškrtni opis, ať se srovná se serverem
onSettled: () => queryClient.invalidateQueries({ queryKey }),
})

getQueryData přečte, co je na nástěnce. setQueryData tam zapíše něco jiného (neměnně, jako stav). Co vrátí onMutate, dostanou další háčky jako context, a tak se záloha dostane do onError.

Výzva

Z useEffect na useQuery + prefetch

StředníBonus · zdarma po přihlášení

Uvidíš, kolik kódu zmizí – a kolik funkcí přibude. Co se má dít, je v komentářích v kódu.

🔓 Bonusová výzva – zdarma po přihlášení

Střední a těžké výzvy se odemknou, jakmile se přihlásíš. Stejně jako celý kurz jsou zdarma – stačí e-mail, žádné heslo ani platba.

Jak nakládáme s tvými údaji
Výzva

Optimistické lajky v cache

TěžkáBonus · zdarma po přihlášení

Kompletní recept na optimistickou aktualizaci s návratem zpět – přesně podle dokumentace TanStack Query. Co se má dít, je v komentářích v kódu.

🔓 Bonusová výzva – zdarma po přihlášení

Střední a těžké výzvy se odemknou, jakmile se přihlásíš. Stejně jako celý kurz jsou zdarma – stačí e-mail, žádné heslo ani platba.

Jak nakládáme s tvými údaji

Shrnutí

  • Serverová data jsou opsaný ceník: nepatří ti a můžou zastarat. Potřebují sdílení, deduplikaci, obnovování a invalidaci.
  • useQuery je správce nástěnky. queryKey je štítek a obsahuje vše, na čem dotaz závisí; staleTime říká, jak dlouho je opis čerstvý.
  • useMutation + invalidateQueries: po změně na serveru přeškrtni staré opisy.
  • Suspense + use()/useSuspenseQuery: čekání a chyby popsané o úroveň výš, na jednom místě.
  • Objednávej všechno najednou a co nejdřív (loadery, prefetch) – vyhni se vodopádům.
🧩 Odkud se berou věci, které tu nejsou definované (QueryClient, QueryClientProvider, useQuery, fetchProducts)
QueryClient, QueryClientProvider, useQuery – z knihovny @tanstack/react-query: knihovna pro načítání dat a cache (lekce React 13).
fetchProducts – ze souboru src/course/fakeApi.ts. Falešné API kurzu: data a funkce, které předstírají server (se zpožděním, občas i s chybou). Ve skutečné aplikaci by tu bylo volání fetch().
export interface Product {
id: number
name: string
category: 'ovoce' | 'zelenina' | 'pečivo' | 'mléčné'
price: number
inStock: boolean
}
export async function fetchProducts(query = '', signal?: AbortSignal): Promise<Product[]> {
await sleep(randomDelay())
signal?.throwIfAborted()
const q = query.trim().toLowerCase()
return products.filter((p) => p.name.toLowerCase().includes(q))
}
CSS třídy jako row, stack, card, list, btn, input nebo muted jsou hotové styly kurzu v src/styles.css (row = prvky vedle sebe, stack = pod sebou, card = rámeček, muted = šedý text).