Formuláře, autentizace a nasazení
Cíl: Vylepšit formuláře a přidat simulaci autentizace. Optimalizovat aplikaci a nasadit ji online.
1️⃣ Validace formulářů
- React Hook Form:
react-hook-form– rychlé a lehké formuláře- Dokumentace: React Hook Form
- Integrace se schématy: React Hook Form - Schema Validation
- Zod: deklarativní validace schémat, generování typů
- Alternativa: TanStack Form (moderní formulářová knihovna)
Tipy pro UX:
- Debounce u onChange validací (např. kontrola dostupnosti jména)
- Throttle pro autosave/drsnější omezení volání API
Minimal příklad RHF + Zod:
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
const schema = z.object({
email: z.string().email(),
password: z.string().min(6),
});
type FormValues = z.infer<typeof schema>;
export function LoginForm({ onSubmit }: { onSubmit: (v: FormValues) => void }) {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormValues>({
resolver: zodResolver(schema),
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input placeholder="Email" {...register("email")} />
{errors.email && <span>{errors.email.message}</span>}
<input placeholder="Heslo" type="password" {...register("password")} />
{errors.password && <span>{errors.password.message}</span>}
<button disabled={isSubmitting} type="submit">Přihlásit</button>
</form>
);
}
startTransition pro neblokující aktualizace stavu
startTransition umožňuje označit aktualizace stavu jako přechodové (Transition), což znamená, že tyto aktualizace neblokují uživatelské rozhraní. To je užitečné při zpracování formulářů, kde některé změny stavu mohou být provedeny na pozadí, aniž by ovlivnily interaktivitu aplikace.
- Dokumentace: startTransition
Příklad použití při přepínání záložek nebo filtrování výsledků:
import { startTransition, useState } from "react";
function SearchForm() {
const [query, setQuery] = useState("");
const [results, setResults] = useState<string[]>([]);
function handleSearch(newQuery: string) {
setQuery(newQuery); // Okamžitá aktualizace inputu (blokující)
startTransition(() => {
// Náročné vyhledávání na pozadí (neblokující)
const filtered = performSearch(newQuery);
setResults(filtered);
});
}
return (
<div>
<input
value={query}
onChange={(e) => handleSearch(e.target.value)}
/>
<ResultsList results={results} />
</div>
);
}
Kdy použít startTransition:
- Aktualizace UI, které nejsou kritické pro okamžitou interakci
- Filtrování nebo vyhledávání velkých datových sad
- Přepínání mezi různými zobrazeními nebo záložkami
- Aktualizace sekundárních částí formuláře
Poznámka: startTransition neposkytuje indikátor načítání. Pokud potřebujete sledovat, zda je přechod stále probíhající, použijte místo toho useTransition hook.
1️⃣.1️⃣ use hook s Fetch a FormData
Hook use v Reactu umožňuje číst hodnotu zdroje, jako je Promise nebo kontext. Při práci s formuláři můžete použít use např. k načtení dat pomocí fetch a zpracování formulářových dat pomocí FormData API.
- Dokumentace: use hook
Použití use s Fetch
import { use, Suspense } from "react";
function UserProfile({ userId }: { userId: string }) {
// Promise vytvořená mimo komponentu nebo předaná jako prop
const userPromise = fetch(`/api/users/${userId}`).then(res => res.json());
const user = use(userPromise);
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
function App() {
return (
<Suspense fallback={<p>Načítám uživatele...</p>}>
<UserProfile userId="123" />
</Suspense>
);
}
Použití use s FormData
import { use, Suspense, useState } from "react";
function SearchResults({ searchPromise }: { searchPromise: Promise<any> }) {
const results = use(searchPromise);
return (
<ul>
{results.map((item: any) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
}
function SearchForm() {
const [searchPromise, setSearchPromise] = useState<Promise<any> | null>(null);
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const query = formData.get("query") as string;
// Vytvoření Promise pro vyhledávání
const promise = fetch(`/api/search?q=${encodeURIComponent(query)}`)
.then(res => res.json());
setSearchPromise(promise);
}
return (
<div>
<form onSubmit={handleSubmit}>
<input name="query" placeholder="Hledat..." />
<button type="submit">Hledat</button>
</form>
{searchPromise && (
<Suspense fallback={<p>Hledám...</p>}>
<SearchResults searchPromise={searchPromise} />
</Suspense>
)}
</div>
);
}
Důležité poznámky:
usemusí být volán v React komponentě nebo hookuusenelze volat v try-catch bloku – místo toho použijte Error Boundary nebo.catch()na Promise- Komponenta volající
uses Promise se automaticky pozastaví (suspend) během čekání na Promise - Pro zpracování chyb použijte Error Boundary nebo Promise
.catch()
1️⃣.2️⃣ useDeferredValue pro optimalizaci výkonu
useDeferredValue umožňuje odložit aktualizaci hodnoty, což je užitečné při optimalizaci výkonu uživatelského rozhraní. Při práci s formuláři může useDeferredValue pomoci udržet interaktivitu aplikace i při zpracování náročných operací.
- Dokumentace: useDeferredValue
- Podrobný článek: Josh Comeau - useDeferredValue
Základní použití
import { useDeferredValue, useMemo, useState } from "react";
function SearchForm() {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => {
// Náročné vyhledávání s odloženou hodnotou
return performExpensiveSearch(deferredQuery);
}, [deferredQuery]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Hledat..."
/>
{/* Input zůstává responzivní, i když se výsledky ještě načítají */}
<SearchResults results={results} isStale={query !== deferredQuery} />
</div>
);
}
Použití s formulářovým filtrováním
import { useDeferredValue, useMemo, useState } from "react";
function ProductFilter() {
const [filter, setFilter] = useState("");
const deferredFilter = useDeferredValue(filter);
const filteredProducts = useMemo(() => {
// Náročné filtrování produktů
return products.filter(product =>
product.name.toLowerCase().includes(deferredFilter.toLowerCase())
);
}, [deferredFilter]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filtrovat produkty..."
/>
{/* Zobrazení indikátoru, pokud jsou výsledky zastaralé */}
{filter !== deferredFilter && (
<p className="text-sm text-gray-500">Aktualizuji výsledky...</p>
)}
<ProductList products={filteredProducts} />
</div>
);
}
Kdy použít useDeferredValue:
- Vyhledávání nebo filtrování velkých datových sad
- Náročné výpočty závislé na uživatelském vstupu
- Situace, kdy chcete zachovat responzivitu inputu při zpracování dat
Rozdíl mezi startTransition a useDeferredValue:
startTransition: označuje aktualizace stavu jako přechodové (používáte při volánísetState)useDeferredValue: odkládá hodnotu pro použití v dalších výpočtech (používáte s hodnotami, které už máte)
2️⃣ Simulovaná autentizace (cookies + JSON Server)
- json-server: rychlý fake backend nad JSON souborem
- Cookies: uložení „session tokenu“ či role (pouze demo!)
Příklad jednoduché „login“ simulace:
import Cookies from "js-cookie";
export async function fakeLogin(email: string, password: string) {
// volitelně volání json-serveru; zde jen demo
if (email === "admin@example.com" && password === "secret123") {
Cookies.set("token", "demo-token", { expires: 1 });
Cookies.set("role", "admin", { expires: 1 });
return { ok: true };
}
Cookies.set("token", "demo-token", { expires: 1 });
Cookies.set("role", "member", { expires: 1 });
return { ok: true };
}
export function logout() {
Cookies.remove("token");
Cookies.remove("role");
}
export function getRole() {
return Cookies.get("role") ?? "guest";
}
3️⃣ Podmíněné zobrazování (role-based access)
- Základ: načíst stav uživatele (role) a podle něj renderovat sekce/route guardy
- React Router: ochrana tras, lazy route guards
Jednoduchý guard:
function RequireRole({ role, children }: { role: "member" | "admin"; children: React.ReactNode }) {
const current = getRole();
if (current !== role && current !== "admin") return <p>Přístup odepřen</p>;
return <>{children}</>;
}
Zmínka: produkční autentizace
- Auth.js (NextAuth.js):
- Better Auth:
- Pro SPA s Vite zvažte backend/edge auth, případně BFF (Backend-for-Frontend)
4️⃣ Lazy loading a code-splitting
Příklad:
const AdminPage = React.lazy(() => import("./pages/AdminPage"));
export function App() {
return (
<React.Suspense fallback={<p>Načítám…</p>}>
<AdminPage />
</React.Suspense>
);
}
5️⃣ Memoizace a výkon
Tipy:
- Stabilní props pro memoizované komponenty
- Vyhnout se zbytečným re-renderům (kontrolovat závislosti)
6️⃣ Testování UI
- React Testing Library:
- Vitest (preferováno s Vite) nebo Jest:
- Storybook: dokumentace komponent + interaktivní testy (Storybook Test Runner)
- Playwright pro e2e:
7️⃣ Build a statický export
- Vite build:
npm run build– výstup vedist/ - Nasazení statického SPA (React Router používá
index.htmlfallback)
8️⃣ Nasazení
Nastavení:
- Build command:
vite build(nebonpm run build) - Publish dir:
dist - SPA fallback (200.html) – u Netlify:
9️⃣ Základy CI/CD
Příklad minimální GitHub Actions workflow (build + lint):
name: ci
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run lint --if-present
- run: npm run build
🔟 Docker (volitelně)
Ukázka jednoduchého Dockerfile pro statický build:
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
✅ Výstup lekce
- Funkční přihlášení s validací a role-based přístupem
- Nasazená aplikace (Vercel/Netlify) se správou přístupu a základními optimalizacemi
- Základy testů a CI/CD pro budoucí rozvoj