Lekce 13 · Ekosystém
Routování (React Router)
Stránky, parametry, vnořené layouty, stav v URL, loadery a chráněné cesty.
Načítám lekci…
Lekce 13 · Ekosystém
Stránky, parametry, vnořené layouty, stav v URL, loadery a chráněné cesty.
Načítám lekci…
Single-page aplikace je jedna jediná HTML stránka. Kdyby to tak zůstalo, měla by celá aplikace jednu adresu – a uživatel by nemohl poslat kolegovi odkaz na konkrétní produkt, dát si stránku do záložek ani se vrátit tlačítkem Zpět. Proto potřebujeme, aby každá „obrazovka“ měla vlastní adresu, i když je to pořád jedna stránka.
Představ si aplikaci jako hotel:
/produkty/42 = „druhé patro, pokoj 42“. Kdo zná číslo, dostane se přímo tam.<Outlet />.?vyhled=more&postylka=ano. Pořád stejný pokoj, jen jinak nachystaný.Malý obchod: domovská stránka, seznam produktů, detail produktu a stránka „nenalezeno“.
import { createBrowserRouter, RouterProvider, Outlet, Link, useParams } from 'react-router'const router = createBrowserRouter([{path: '/',element: <Layout />, // chodba – je vidět vždychildren: [{ index: true, element: <Home /> }, // přesně "/"{ path: 'produkty', element: <ProductList /> },{ path: 'produkty/:productId', element: <ProductDetail /> },{ path: '*', element: <NotFound /> }, // cokoli jiného],},])createRoot(root).render(<RouterProvider router={router} />)function Layout() {return (<><nav><Link to="/">Domů</Link><Link to="/produkty">Produkty</Link></nav><Outlet /> {/* dveře pokoje */}</>)}
Uživatel klikne na odkaz vedoucí na /produkty/3. Co udělá recepce:
<Link> nezpůsobí obnovení stránky. Jen změní adresu v prohlížeči a dá vědět routeru./ sedí na začátek adresy → vykreslí Layout. V jeho children najde produkty/:productId, které sedí na zbytek.Layout se vykreslí s navigací a místo <Outlet /> se objeví ProductDetail. Navigace zůstala stejná – změnil se jen pokoj.ProductDetail zjistíme číslo pokoje přes useParams():function ProductDetail() {const { productId } = useParams() // "3" – vždy TEXT, ne číslo!const product = products.find((p) => p.id === Number(productId))if (!product) return <p>Produkt nenalezen</p>return <h2>{product.name}</h2>}
Dvojtečka v :productId říká: „tady může být cokoli a zapamatuj si to pod tímhle jménem“. Číslo na dveřích pokoje je ale vždycky napsané – je to text. Proto Number(productId).
Co vidíš: Obchod z příkladu výše. Nahoře falešný adresní řádek se šipkami Zpět/Vpřed, pod ním navigace (layout) a v kartě obsah aktuální stránky (<Outlet />).
Vyzkoušej:
/produkty, tlačítko se zvýrazní (to umí NavLink) a v kartě je seznam. Navigace nahoře zůstala.* a ukáže se 404.Co z toho plyne: Každá obrazovka má svou adresu, layout se nemění a mění se jen obsah v <Outlet />. A všechno bez obnovení stránky.
Načítám interaktivní část…
| API | K čemu |
|---|---|
<Link to> | Navigace bez obnovení stránky. Relativní cesty se řeší vůči aktuální routě. |
<NavLink> | Link, který ví, že je aktivní (menu, záložky). |
<Outlet /> | Místo v layoutu, kam se vykreslí vnořená routa. |
useParams() | Proměnné části adresy (:id). Vždy text – převeď si je! |
useNavigate() | Navigace z kódu (po odeslání formuláře, po přihlášení). |
<Navigate to replace /> | Přesměrování při renderu (chráněné stránky). |
useLocation() | Aktuální adresa: pathname, search, state. |
Když si v hotelu objednáš pokoj „s výhledem na moře a postýlkou“, chceš, aby to platilo i zítra a aby to kolega, kterému pošleš rezervaci, dostal stejně. Přání proto patří do rezervace, ne do tvé hlavy.
Filtry, řazení, stránkování, hledaný výraz – to všechno jsou přání k obsahu stránky. Kdyby byla jen v useState, po obnovení stránky by zmizela a kolega by po otevření odkazu viděl něco jiného. Patří proto do search params – části adresy za otazníkem: /produkty?kategorie=ovoce&razeni=cena.
function Catalog() {const [searchParams, setSearchParams] = useSearchParams()// Čtení: jako z mapy, chybějící hodnota je nullconst category = searchParams.get('kategorie') // "ovoce" | nullconst sort = searchParams.get('razeni') ?? 'nazev'// Zápis: změní adresu → komponenta se překreslí s novými hodnotamifunction setCategory(value: string | null) {setSearchParams((prev) => {const next = new URLSearchParams(prev)if (value) next.set('kategorie', value)else next.delete('kategorie')return next})}const visible = products.filter((p) => !category || p.category === category)// …}
useSearchParams funguje podobně jako useState: vrátí aktuální hodnoty a funkci na změnu. Jen „sešitem“ je adresa.new URLSearchParams(prev)) a měníme jen jeden – ostatní přání zůstanou.Co vidíš: Katalog s hledáním, kategoriemi a řazením. Všechny tři filtry žijí jen v adrese – sleduj adresní řádek nahoře. Ukázka začíná na adrese /?kategorie=ovoce, jako by ti ji někdo poslal.
Vyzkoušej:
q se mění s každým písmenem, ale historii nezaplevelí (kód používá replace).Co z toho plyne: Stav v adrese přežije obnovení stránky, jde poslat odkazem a funguje s tlačítky Zpět a Vpřed. Komponenta ho jen čte a počítá z něj.
Načítám interaktivní část…
Ve špatném hotelu tě pošlou do pokoje a pokojská začne uklízet, až když vejdeš. Čekáš ve dveřích. V dobrém hotelu je pokoj připravený dřív, než do něj vstoupíš.
Bez loaderu načítá data komponenta sama – v efektu, až když se vykreslí. Uživatel nejdřív uvidí prázdnou stránku se „Načítám…“, a když má stránka vnořené části, každá začne načítat až po té nadřazené (tzv. vodopád). Loader je pokojská: router ho zavolá před vykreslením stránky a komponenta dostane data hotová.
// Pokojská: načte data dřív, než se stránka ukážeasync function productLoader({ params }: LoaderFunctionArgs) {const product = await fetchProduct(Number(params.id))if (!product) throw new Response('Produkt nenalezen', { status: 404 })return { product }}// Routa: k pokoji přiřadíme pokojskou a „recepční pro problémy“{ path: 'produkt/:id', loader: productLoader, element: <ProductPage />, errorElement: <ErrorPage /> }// Komponenta: žádný useEffect, žádné „Načítám…“ – data už jsoufunction ProductPage() {const { product } = useLoaderData<typeof productLoader>()return <h2>{product.name}</h2>}
Co se stane po kliknutí na odkaz na produkt:
useNavigation().state rovno 'loading' – můžeš ukázat ukazatel průběhu.useLoaderData.Response s 404), místo stránky se vykreslí errorElement – recepce řekne „takový pokoj nemáme“.Co vidíš: Seznam produktů a detail, obojí s loaderem. Falešné API odpovídá se zpožděním. Nad kartou se během navigace ukáže „Načítám další stránku…“.
Vyzkoušej:
Co z toho plyne: Data se načtou před vykreslením. Komponenta řeší jen zobrazení a chyby má na starosti errorElement.
Načítám interaktivní část…
| Režim React Routeru | Co to je | Kdy |
|---|---|---|
| Declarative | <BrowserRouter> + <Routes>/<Route> v JSX | Jednoduché aplikace, postupná migrace. |
| Data | createBrowserRouter + loader/action/errorElement | SPA, kde chceš načítání dat svázané s routami. |
| Framework | Vite plugin, routy podle souborů, SSR, typegen | Nové full-stack aplikace (nástupce Remixu). |
Na VIP patro hotelu se nedostaneš jen tak – u výtahu stojí vrátný. Když nemáš kartu, pošle tě na recepci, a ta si poznamená, kam jsi chtěl. Po vyřízení tě pošle rovnou tam.
V routeru je vrátný layout routa bez vlastní adresy. Obalí chráněné stránky a rozhodne, jestli pustí dál (<Outlet />), nebo přesměruje na přihlášení:
function RequireAuth() {const { user } = useAuth()const location = useLocation()if (!user) {// na recepci – a poznamenej si, kam host chtělreturn <Navigate to={`/login?redirect=${location.pathname}`} replace />}return <Outlet /> // pusť dál}// Routa bez path: nepřidá nic do adresy, jen obalí děti{ element: <RequireAuth />, children: [{ path: 'ucet', element: <Account /> },{ path: 'ucet/nastaveni', element: <Settings /> },]}
replace zařídí, že se přesměrování nezapíše do historie – tlačítko Zpět pak nevede znovu k vrátnému. Dokončit přihlášení s návratem tam, kam uživatel chtěl, je úkolem druhé výzvy.
Dvě stránky, dva odkazy. Sleduj, jak se mění adresa nahoře.
VÝZVA: Odkaz na druhou stránku Routy jsou hotové: '/' (Domů) a '/o-nas' (O nás). Chybí odkazy mezi nimi. 1. Na stránce Home doplň <Link to="/o-nas">O nás</Link> 2. Na stránce About doplň <Link to="/">Zpět domů</Link>
Načítám interaktivní část…
Mini e-shop procvičí kapitoly 1 a 2: layout, seznam, detail, filtr v adrese a 404. Chráněné stránky dotáhnou kapitolu 4 – vrátného i návrat tam, kam uživatel původně chtěl.
Kompletní routování malé aplikace: layout, seznam, detail, stav v adrese a 404.
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
Vzor, který najdeš v každé aplikaci s přihlášením: vrátný + návrat tam, kam uživatel původně chtěl.
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
<Outlet /> = společný layout.Link/NavLink v JSX, useNavigate v handlerech, <Navigate /> při renderu. Nikdy <a href>.errorElement řeší chyby a 404.<Navigate replace> na přihlášení a návrat zpět.createMemoryRouter, Link, Outlet, RouterProvider, AddressBar)createMemoryRouter, Link, Outlet, RouterProvider – z knihovny react-router: knihovna pro routování (lekce 13).AddressBar – ze souboru src/lessons/13-routing/demos/AddressBar.tsx. Falešný adresní řádek. Ukázky běží v paměťovém routeru, takže by jinak URL nebyla vidět./** Falešný adresní řádek – ukázky běží v paměťovém routeru, tak ať je vidět URL. */export function AddressBar() {const location = useLocation()const navigate = useNavigate()return (<div className="row" style={{ marginBottom: 12 }}><button className="btn btn-small btn-ghost" onClick={() => navigate(-1)} aria-label="Zpět">←</button><button className="btn btn-small btn-ghost" onClick={() => navigate(1)} aria-label="Vpřed">→</button><code style={{ flex: 1, padding: '0.3rem 0.6rem' }}>https://obchod.cz{location.pathname}{location.search}</code></div>)}
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).