Start / Cheat Sheets

Cheat Sheet

React v19 Ökosystem — Cheat Sheet

Stand: · React v19 Grundlagen & Moderne Features

ReactTanStackZodZustand

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.

Module: shadcn/ui · Base UI

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

Zum Seminar React v19 Grundlagen & Moderne Features