Die Bibliotheken rund um React aus dem Seminar React v19 Grundlagen & Moderne Features — eine Kurzform je Werkzeug. Den Kern (Hooks, Vite, Routing, Formulare, Vitest) trägt das Kern-Cheat-Sheet.
Diese Bibliotheken bewegen sich schnell. Der Stand ist oben vermerkt; im Zweifel gilt die jeweilige Projektdoku.
TanStack Router
const router = createRouter({ routeTree, defaultPreload: 'intent' })
declare module '@tanstack/react-router' {
interface Register { router: typeof router } // ohne dies keine Typinferenz
}
// routes/posts.$postId.tsx → /posts/$postId
<Link to="/posts/$postId" params={{ postId: post.id }}>{post.title}</Link>
Search Params sind First-Class State und werden validiert:
validateSearch: z.object({ page: z.number().catch(1), q: z.string().catch('') })
beforeLoad: ({ context }) => {
if (!context.auth.user) throw redirect({ to: '/login' })
},
loader: ({ context }) => context.queryClient.ensureQueryData(postsQuery),
Fallen: routeTree.gen.ts nach dem Umbenennen neu generieren · loaderDeps
vergessen → Wechsel der Search-Params lädt nicht neu · $param und Splat-Route
$ verwechseln.
Modul: TanStack Router
TanStack Query
const { data, isPending, isError } = useQuery({
queryKey: ['todos'],
queryFn: () => axios.get('/todos').then(r => r.data),
})
// Query-Keys zentral halten statt im Komponentenbaum verstreuen
const todoQueries = {
list: (filter: string) =>
queryOptions({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter) }),
}
const qc = useQueryClient()
const add = useMutation({
mutationFn: (t) => axios.post('/todos', t),
onSuccess: () => qc.invalidateQueries({ queryKey: ['todos'] }),
})
// Pagination ohne Flackern
useQuery({ queryKey: ['todos', page], queryFn: () => fetchPage(page),
placeholderData: keepPreviousData })
Fallen: QueryClientProvider vergessen → der Hook wirft · staleTime überall 0
→ Refetch-Fluten · gcTime mit staleTime verwechseln · mutate awaiten
wollen (dafür gibt es mutateAsync) · 4xx blind retryen.
Modul: TanStack Query v5
Zustand
import { create } from 'zustand'
const useCounter = create(set => ({
count: 0,
inc: () => set(s => ({ count: s.count + 1 })), // Updater, nicht ganzes Objekt
}))
const count = useCounter(s => s.count) // atomar selektieren
import { useShallow } from 'zustand/react/shallow'
const { a, b } = useStore(useShallow(s => ({ a: s.a, b: s.b })))
// Persistenz gezielt, nicht den ganzen Store
persist<Settings>((set) => ({ theme: 'dark' }),
{ name: 'settings', partialize: (s) => ({ theme: s.theme }) })
Modul: Zustand
Axios
const api = axios.create({ baseURL: '/api' })
api.interceptors.request.use(cfg => {
cfg.headers.Authorization = `Bearer ${getToken()}`
return cfg
})
// Abbruch im Effekt — sonst schreibt eine alte Antwort in neuen State
useEffect(() => {
const ac = new AbortController()
axios.get('/search', { signal: ac.signal }).then(r => setData(r.data))
return () => ac.abort()
}, [query])
Fallen: res statt res.data verwenden · isAxiosError weglassen →
untypisierter catch · responseType bei Blobs vergessen → kaputte Datei ·
abgebrochene Requests als echten Fehler melden.
Modul: Axios
Zod
const User = z.object({ id: z.uuid(), email: z.email() })
type User = z.infer<typeof User>
// getaggte Varianten: discriminatedUnion, nicht union
const Result = z.discriminatedUnion('status', [
z.object({ status: z.literal('ok'), data: User }),
z.object({ status: z.literal('err'), msg: z.string() }),
])
// In React safeParse — parse wirft mitten im Render
const r = Form.safeParse(values)
if (!r.success) setErrors(z.flattenError(r.error).fieldErrors)
// An der API-Grenze validieren
return User.parse(res.data)
Fallen: z.input und z.output verwechseln · any-Cast direkt nach parse ·
.pick()/.omit() auf Schemas mit Refinements · strict-Mode aus → die
Typinferenz wird unzuverlässig.
Modul: Zod
TanStack Form
const form = useForm({ defaultValues: { email: '' } }) // ohne sie keine Inferenz
<form.Field name="email" validators={{ onChange: z.email('Ungültige E-Mail') }}>
{field => (
<input value={field.state.value}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur} />
)}
</form.Field>
// Nur den Button neu rendern, nicht das Formular
<form.Subscribe selector={s => s.canSubmit}>
{canSubmit => <button disabled={!canSubmit}>Speichern</button>}
</form.Subscribe>
handleBlur vergessen → touched bleibt false, Fehler erscheinen zur
falschen Zeit.
Modul: TanStack Form
shadcn/ui und Base UI
npx shadcn init --base base-ui
npx shadcn add button input card dialog
npx shadcn diff # Bestand gegen Registry vergleichen
// shadcn: asChild reicht den Style ans eigene Element durch
<Button asChild><a href="/docs">Zur Doku</a></Button>
// Base UI: composable Parts, und render statt asChild
<Dialog.Root>
<Dialog.Trigger render={<MyButton />}>Öffnen</Dialog.Trigger>
<Dialog.Portal><Dialog.Popup>Inhalt</Dialog.Popup></Dialog.Portal>
</Dialog.Root>
// Zustände über data-Attribute stylen
<Switch.Root className="bg-gray-300 data-[checked]:bg-primary" />
Fallen: asChild mit mehreren Kindern → Runtime-Fehler · bei Base UI asChild
erwarten statt render · keepMounted vergessen → keine Exit-Animation ·
components.json-Aliases passen nicht zur Ordnerstruktur.
i18next
i18next.use(HttpBackend).use(LanguageDetector).use(initReactI18next)
.init({ fallbackLng: 'de', defaultNS: 'common' })
const { t } = useTranslation('common')
<h1>{t('welcome', { name })}</h1>
<Trans i18nKey="terms">Ich akzeptiere die <a href="/agb">AGB</a>.</Trans>
{ "cart_item_one": "{{count}} Artikel", "cart_item_other": "{{count}} Artikel" }
// Aufruf: t('cart_item', { count: n })
i18n heißt: die App übersetzbar machen (Keys statt Text). l10n heißt: sie
für eine Locale füllen. Fallen: fallbackLng vergessen → leere Texte bei Lücken
· alle Namespaces eager laden → großes Bundle.
Modul: i18n mit react-i18next
ESLint
Unter ESLint 9 gilt die Flat Config — .eslintrc* wird nicht mehr gelesen. Die
Einführung schrittweise fahren, nicht alle Regeln auf einmal als error
(„Big Bang”). In TypeScript-Projekten prop-types abschalten, sonst rauscht es.
Und --fix nicht blind laufen lassen: Das Ergebnis gehört ins Review.
Modul: ESLint-Regeln