Back

React & TypeScript: Bewährte Patterns für saubereren Code

React & TypeScript: Bewährte Patterns für saubereren Code

Beim Entwickeln von React-Anwendungen mit TypeScript begegnen Sie immer wieder denselben Herausforderungen: Wie typisiert man Props korrekt, behandelt Events sicher und strukturiert Komponenten für maximale Wiederverwendbarkeit? Diese Patterns dienen nicht nur dazu, den Compiler zufriedenzustellen – sie helfen dabei, Code zu schreiben, der einfacher zu verstehen, zu refactoren und zu warten ist.

Dieser Artikel behandelt die praktischsten React TypeScript-Patterns, die Sie täglich verwenden werden. Sie lernen, wie Sie Komponenten effektiv typisieren, optionale Props handhaben, mit Events und Refs arbeiten, Utility-Types nutzen und Discriminated Unions in Reducern verwenden. Jedes Pattern enthält fokussierte Beispiele, die genau zeigen, warum es Ihren Code verbessert.

Wichtige Erkenntnisse

  • Props und State explizit typisieren mit Interfaces oder Types für bessere IntelliSense und Fehlererkennung
  • Optionale und Standard-Props handhaben mit TypeScripts optionalen Eigenschaften und Standardparametern
  • Utility-Types verwenden wie Pick und ComponentProps, um Wiederholungen zu reduzieren und Konsistenz zu gewährleisten
  • Events und Refs korrekt typisieren für typsichere DOM-Interaktionen
  • Discriminated Unions nutzen in Reducern für erschöpfende Action-Behandlung
  • Komponenten mit forwardRef komponieren, um Typsicherheit beim Weiterreichen von Refs zu gewährleisten

Typisierung von Props und State

Das Fundament der React TypeScript-Patterns beginnt mit der korrekten Typisierung Ihrer Komponenten-Props. Dies fängt Fehler zur Compile-Zeit ab und bietet bessere Autovervollständigung in Ihrem Editor.

Grundlegendes Props-Pattern

interface ButtonProps {
  label: string
  onClick: () => void
  disabled?: boolean
}

function Button({ label, onClick, disabled = false }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  )
}

Dieses Pattern stellt sicher, dass Ihre Komponenten die korrekten Datentypen erhalten. Das Interface dokumentiert klar, welche Props die Komponente erwartet, wodurch sie selbstdokumentierend wird.

State mit useState typisieren

TypeScript leitet State-Typen normalerweise korrekt ab, aber komplexer State profitiert von expliziter Typisierung:

import { useState } from 'react'

interface User {
  id: string
  name: string
  email: string
}

function UserProfile() {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(false) // Typ wird als boolean abgeleitet

  // TypeScript weiß, dass user null sein könnte
  return user ? <div>{user.name}</div> : <div>Loading...</div>
}

Umgang mit optionalen und Standard-Props

React-Komponenten haben oft optionale Konfigurations-Props. TypeScripts optionale Eigenschaften kombiniert mit Standardparametern schaffen ein sauberes Pattern:

interface CardProps {
  title: string
  description?: string
  variant?: 'primary' | 'secondary'
  className?: string
}

function Card({ 
  title, 
  description, 
  variant = 'primary',
  className = ''
}: CardProps) {
  return (
    <div className={`card card-${variant} ${className}`}>
      <h2>{title}</h2>
      {description && <p>{description}</p>}
    </div>
  )
}

Dieses Pattern macht Komponenten flexibel und behält dabei die Typsicherheit bei. Erforderliche Props sind klar ersichtlich, und optionale Props haben sinnvolle Standardwerte.

Typisierung von Events und Refs

Event-Handler und Refs sind häufige Quellen für TypeScript-Fehler. Diese Patterns eliminieren das Rätselraten:

Event-Handler-Pattern

import { useState, ChangeEvent, FormEvent } from 'react'

interface FormProps {
  onSubmit: (data: { email: string; password: string }) => void
}

function LoginForm({ onSubmit }: FormProps) {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value)
  }

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    onSubmit({ email, password })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" value={email} onChange={handleEmailChange} />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      <button type="submit">Login</button>
    </form>
  )
}

Ref-Pattern

import { useRef, useEffect } from 'react'

function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    // TypeScript weiß, dass inputRef.current null sein könnte
    inputRef.current?.focus()
  }, [])

  return <input ref={inputRef} type="text" />
}

Verwendung von Utility-Types

TypeScripts Utility-Types reduzieren Code-Duplikation und halten Typen in Ihrer Anwendung synchronisiert.

ComponentProps-Pattern

Extrahieren Sie Props von bestehenden Komponenten, ohne Typdefinitionen zu wiederholen:

import { ComponentProps } from 'react'

// Alle Props abrufen, die ein button-Element akzeptiert
type ButtonProps = ComponentProps<'button'> & {
  variant?: 'primary' | 'secondary'
}

function StyledButton({ variant = 'primary', ...props }: ButtonProps) {
  return <button className={`btn-${variant}`} {...props} />
}

// Props von eigenen Komponenten extrahieren
type MyButtonProps = ComponentProps<typeof StyledButton>

Pick-Pattern

Wählen Sie spezifische Props beim Erweitern von Komponenten aus:

interface UserData {
  id: string
  name: string
  email: string
  role: string
  lastLogin: Date
}

// Nur einige Eigenschaften für die Karte benötigt
type UserCardProps = Pick<UserData, 'name' | 'email'> & {
  onClick?: () => void
}

function UserCard({ name, email, onClick }: UserCardProps) {
  return (
    <div onClick={onClick}>
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  )
}

Komponenten-Komposition mit forwardRef

Beim Erstellen von Komponenten-Bibliotheken oder beim Wrappen nativer Elemente behält forwardRef die Typsicherheit bei und stellt Refs zur Verfügung:

import { forwardRef, useRef, InputHTMLAttributes } from 'react'

interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
  label: string
  error?: string
}

const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, ...props }, ref) => {
    return (
      <div>
        <label>{label}</label>
        <input ref={ref} {...props} />
        {error && <span className="error">{error}</span>}
      </div>
    )
  }
)

Input.displayName = 'Input'

// Verwendung behält vollständige Typsicherheit bei
function Form() {
  const inputRef = useRef<HTMLInputElement>(null)
  
  return <Input ref={inputRef} label="Email" type="email" />
}

Discriminated Unions in Reducern

Discriminated Unions machen Reducer-Actions typsicher und ermöglichen erschöpfende Prüfung:

interface TodoState {
  items: Array<{ id: string; text: string; done: boolean }>
  filter: 'all' | 'active' | 'completed'
}

type TodoAction =
  | { type: 'ADD_TODO'; payload: string }
  | { type: 'TOGGLE_TODO'; payload: string }
  | { type: 'SET_FILTER'; payload: TodoState['filter'] }
  | { type: 'CLEAR_COMPLETED' }

function todoReducer(state: TodoState, action: TodoAction): TodoState {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        items: [...state.items, {
          id: Date.now().toString(),
          text: action.payload,
          done: false
        }]
      }
    
    case 'TOGGLE_TODO':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload
            ? { ...item, done: !item.done }
            : item
        )
      }
    
    case 'SET_FILTER':
      return { ...state, filter: action.payload }
    
    case 'CLEAR_COMPLETED':
      return {
        ...state,
        items: state.items.filter(item => !item.done)
      }
    
    default:
      // TypeScript stellt sicher, dass alle Fälle behandelt werden
      const exhaustive: never = action
      return state
  }
}

Dieses Pattern stellt sicher, dass Sie alle Action-Typen behandeln. Wenn Sie einen neuen Action-Typ hinzufügen, wird TypeScript einen Fehler ausgeben, bis Sie ihn im Reducer behandeln.

React 19-Updates

Mit React 19 machen neue Funktionen wie Actions (useActionState, useFormStatus), die use()-API für das Konsumieren von Promises oder Context und die erstklassige Unterstützung für Server Components die Typsicherheit noch wichtiger. Beim Typisieren von Actions definieren Sie explizit die Form von FormData und den Rückgabewert der Action, damit Server- und Client-Code synchron bleiben. Die kommende Umstellung von forwardRef auf die Übergabe von ref als Standard-Prop vereinfacht auch die Typisierung – behandeln Sie ref wie jede andere Prop und lassen Sie TypeScript die Korrektheit über Komponentengrenzen hinweg sicherstellen.

Fazit

Diese React TypeScript-Patterns bilden das Fundament typsicherer React-Anwendungen. Durch explizite Typisierung von Props und State, korrekte Behandlung von Events und Refs und die Nutzung von TypeScripts Utility-Types erstellen Sie Code, der sowohl sicherer als auch wartbarer ist. Die Patterns funktionieren zusammen – ordnungsgemäße Prop-Typisierung ermöglicht bessere Komponenten-Komposition, während Discriminated Unions erschöpfende Behandlung in komplexer State-Logik gewährleisten.

Beginnen Sie mit grundlegender Prop-Typisierung und integrieren Sie schrittweise Utility-Types und erweiterte Patterns, während Ihre Anwendung wächst. Die Investition in ordnungsgemäße Typisierung zahlt sich durch bessere IDE-Unterstützung, weniger Laufzeitfehler und klarere Komponenten-Verträge aus.

Häufig gestellte Fragen

Verwenden Sie interface, wenn Sie möglicherweise später Deklarationen erweitern oder zusammenführen müssen. Verwenden Sie type für Unions, Intersections oder bei der Arbeit mit Utility-Types. Beide funktionieren gut für grundlegende Props, also wählen Sie einen Ansatz und bleiben Sie konsistent.

Verwenden Sie ReactNode für maximale Flexibilität: children: ReactNode. Dies akzeptiert Strings, Zahlen, Elemente, Fragmente und Arrays. Für spezifischere Fälle verwenden Sie ReactElement oder spezifische Element-Typen.

Verwenden Sie generische Funktionen, die die ursprünglichen Prop-Typen der Komponente beibehalten und gleichzeitig neue hinzufügen. Der ComponentProps Utility-Type hilft dabei, bestehende Komponenten-Typen zu extrahieren und zu erweitern, ohne Wiederholungen.

Typisieren Sie Funktionsparameter, Rückgabetypen für exportierte Funktionen und komplexen State explizit. Lassen Sie TypeScript einfache Fälle wie useState mit Primitiven oder offensichtliche Event-Handler ableiten, um Weitschweifigkeit zu reduzieren.

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers