Lekce 14 · Ekosystém
Načítání dat
Proč fetch v useEffect nestačí, TanStack Query, Suspense a use().
Načítám lekci…
Lekce 14 · Ekosystém
Proč fetch v useEffect nestačí, TanStack Query, Suspense a use().
Načítám lekci…
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á:
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.
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 = falsefetchUser(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:
UserName třikrát se stejným id, odejdou tři stejné požadavky.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:
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…
// main.tsx – jednou pro celou aplikaci: zřídíme nástěnkuconst queryClient = new QueryClient()<QueryClientProvider client={queryClient}><App /></QueryClientProvider>// kdekoli v aplikacifunction UserName({ id }: { id: number }) {const { data, isPending, isError, error } = useQuery({queryKey: ['user', id], // štítek: co přesně chciqueryFn: () => 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:
['user', 2]. Na nástěnce nic není → správce zavolá queryFn. Komponenta je ve stavu isPending.staleTime, správce ho zároveň na pozadí obnoví.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ý šanonqueryFn: () => fetchProducts({ category, page }),})useQuery({queryKey: ['products'], // ❌ všechny stránky by sdílely jeden šanonqueryFn: () => fetchProducts({ category, page }),})
| Pojem | Význam |
|---|---|
queryKey | Štítek na šanonu. Obsahuje vše, na čem queryFn závisí. |
staleTime | Jak dlouho je údaj čerstvý (výchozí 0 → obnoví se při každém připojení nebo návratu do okna). |
gcTime | Jak dlouho drží nástěnka data, která nikdo nepoužívá (výchozí 5 min). |
isPending | Ještě nemáme žádná data (první načtení). |
isFetching | Právě běží požadavek – i na pozadí, když už data máme. |
placeholderData | Co ukázat, než přijdou data – např. keepPreviousData při stránkování. |
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 serveruonSuccess: () => {// 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>)}
mutation.mutate(…) zavolá createPost. Během čekání je isPending true.invalidateQueries označí šanony ['posts', …] za zastaralé – všechny stránky seznamu najednou.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:
keepPreviousData nic neblikne.posts.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…
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é undefinedreturn <h2>{user.name}</h2>}<ErrorBoundary fallback={<p>Uživatel nenalezen</p>}><Suspense fallback={<p>Připravujeme…</p>}><UserProfile userPromise={getUser(id)} /></Suspense></ErrorBoundary>
use(userPromise) se podívá, jestli je Promise hotový. Pokud ne, komponenta řekne „ještě čekám“ a React ukáže nejbližší fallback.use tentokrát vrátí data.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:
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ůsob | Kdy |
|---|---|
useQuery | Výchozí volba. Čekání a chyby řešíš v komponentě. |
useSuspenseQuery | Totéž 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. |
Načti seznam produktů přes TanStack Query. Načítání i seznam jsou hotové.
VÝZVA: První useQuery
ProductNames zatím data nenačítá – data jsou prázdná.
1. Nahraď dva řádky s TODO jedním voláním:
const { data, isPending } = useQuery({ queryKey: ['products'], queryFn: () => fetchProducts() })
Načítání i seznam jsou už napsané.Načítám interaktivní část…
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.
Uvidíš, kolik kódu zmizí – a kolik funkcí přibude.
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
Kompletní recept na optimistickou aktualizaci s návratem zpět – přesně podle dokumentace TanStack Query.
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
queryKey je štítek a obsahuje vše, na čem dotaz závisí; staleTime říká, jak dlouho je opis čerstvý.QueryClient, QueryClientProvider, useQuery, fetchProducts)QueryClient, QueryClientProvider, useQuery – z knihovny @tanstack/react-query: knihovna pro načítání dat a cache (lekce 14).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: numbername: stringcategory: 'ovoce' | 'zelenina' | 'pečivo' | 'mléčné'price: numberinStock: 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))}
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).