Startseite / Artikel / Checkliste für eine React-Code-Review: Abgeleiteter Zustand, Reduzierer, Effects und Memo

Checkliste für eine React-Code-Review: Abgeleiteter Zustand, Reduzierer, Effects und Memo

Erlernen Sie, sieben häufige Probleme bei React-Reviews zu erkennen – von doppeltem State und manuellem Abrufen über Fehlverwendung von Effects bis hin zu übermäßiger Memoisierung – sowie, was man stattdessen schreiben sollte.

1906 Wörter

Entwickler mit einigen Jahren Erfahrung in React wissen in der Regel, wie Props fließen, wie Hooks funktionieren und warum der State niemals direkt verändert werden sollte. Dennoch tauchen bei Code-Reviews in ihren Pull Requests immer wieder die gleichen Probleme auf: Komponenten, die zwar funktionieren, aber anfällig sind, schwer zu testen, überdimensioniert oder schwierig erweiterbar. Um diese Phase zu überwinden, geht es weniger darum, obskure APIs zu lernen, sondern vielmehr darum, das Verständnis dafür zu schärfen, wo der State liegt, wofür Effekte dienen und wann Optimierungen sich lohnen. Diese Checkliste geht sieben häufig auftretende Probleme durch, verwendet dazu eine To-Do-Liste in TypeScript als Beispiel und zeigt das Muster, nach dem man in jedem Fall vorgehen sollte.

1. State, der abgeleitet werden sollte

Die grundlegende Regel für den React-State ist es, eine einzige Quelle der Wahrheit beizubehalten. Wenn dieselben Informationen an zwei Orten gespeichert sind, werden die Kopien letztendlich auseinanderdriften.

Wie es aussieht

Eine To-Do-Liste, die sowohl alle Aufgaben als auch die abgeschlossenen Aufgaben benötigt, weist oft zwei Arrays im Zustand auf:

// ❌ BAD: Storing derived state in two separate places
const [todos, setTodos] = useState<Todo[]>([]);
const [completedTodos, setCompletedTodos] = useState<Todo[]>([]);

Jeder Änderung muss daher zweimal nachgegangen werden. Wenn ein Benutzer eine Aufgabe umbenennt und der Code todos aktualisiert, aber completedTodos vergisst, zeigt die Benutzeroberfläche den alten Titel in einer Liste an und den neuen in der anderen.

Was stattdessen geschrieben werden sollte

Bewahren Sie ein Array im Zustand auf und berechnen Sie den abgeschlossenen Teil beim Rendern:

// ✅ GOOD: Single source of truth with derived state
const [todos, setTodos] = useState<Todo[]>([]);
// Derived value recalculated on every render (inexpensive computation)
const completedTodos = todos.filter((todo) => todo.completed);

Das Filtern eines Arrays bei jedem Render ist bei typischen Größen unproblematisch, und die daraus resultierende Liste kann niemals aus dem Takt geraten, da sie jedes Mal neu aus der Quelle berechnet wird. Wenn die Berechnung tatsächlich aufwändig wird – beispielsweise beim Filtern von Zehntausenden von Elementen –, sollten Sie sie in useMemo einschließen; ansonsten ist eine einfache Expression klarer.

2. Manuelle Datenerfassung in useEffect

Manuelle fetch-Aufrufe innerhalb von useEffect erscheinen einfach, führen aber oft zu Rennbedingungen, verfügen über kein Caching und erfordern, dass Lade- sowie Fehlerzustände in jedem Komponenten manuell verwaltet werden.

Wie es aussieht

Dieser Effekt lädt die Liste einmal und verwendet ein isMounted-Flag, damit kein Zustand nach dem Entfernen der Komponente gesetzt wird:

// ❌ BAD: Boilerplate-heavy, error-prone, no caching or deduplication
useEffect(() => {
  let isMounted = true;
  fetch('/api/todos')
    .then((res) => res.json())
    .then((data) => {
      if (isMounted) setTodos(data);
    });
  return () => { isMounted = false; };
}, []);

Es gibt dennoch Mängel. Es gibt keine Fehlerbehandlung, keinen Ladezustand, keine Deduplizierung, wenn zwei Komponenten dieselben Daten anfordern, kein Caching zwischen den Bildschirmen und keine Wiederholungsversuche. Das isMounted-Flag unterdrückt lediglich die späte Aktualisierung; es storniert den Anfrage nicht, wie es ein AbortController tun würde.

Was stattdessen geschrieben werden sollte

Nutzen Sie eine für den Serverzustand konzipierte Bibliothek wie TanStack Query, SWR oder RTK Query. Der entsprechende Aufruf von TanStack Query deklariert einen Cache-Schlüssel sowie eine Abfragefunktion und erhält anschließend die Daten sowie den Lade- und Fehlerzustand zurück:

// ✅ GOOD: Declarative, automatically cached, auto-retried
const { data: todos, isLoading, error } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
});

Bibliotheken wie diese kümmern sich standardmäßig um die Hintergrund-Neuvalidierung, die Deduplizierung von Anfragen, Wiederholungsversuche sowie den Cache-Teilgebrauch und bieten zudem strukturierte Unterstützung für optimistische Aktualisierungen sowie die Integration mit Fehlergrenzen. Der Komponentencode reduziert sich somit auf eine Beschreibung dessen, was benötigt wird.

3. Eine Ansammlung von useState-Aufrufen, wo eigentlich ein Reducer passt

useState eignet sich ideal für einfache Werte wie Schalter oder Textfelder. Sobald Updates von vorherigen Werten abhängen oder der Zustand verschachtelte Arrays und Objekte enthält, die durch mehrere verschiedene Aktionen geändert werden, führt eine Sammlung von Settern dazu, dass die Komponenten überdimensioniert werden und Fehler aufgrund veralteten Zustands auftreten können.

So sieht es aus

Ein einzelner Komponente deklariert fünf bis zehn useState-Hooks und aktualisiert sie über zahlreiche Callbacks, um Elemente hinzuzufügen, zu bearbeiten, umzuschalten und zu entfernen. Die Geschäftsregeln sind dadurch überall in den Ereignishandlern verstreut.

Was stattdessen geschrieben werden sollte

useReducer verlagert die Zustandsübergänge aus der Komponente in eine einzige Funktion. Der untenstehende Reducer verwendet eine diskriminierte Union für seine Aktionen, sodass TypeScript den Payload-Typ für jeden Fall kennt. Beim Hinzufügen eines To-Do-Eintrags wird ein neues Objekt mit einer generierten ID hinzugefügt; das Umschalten durchläuft die Liste und ändert den Zustand des entsprechenden Elements. Beachten Sie, dass die im Code angezeigte Umschaltzeile einen Tippfehler enthält: ...todo.completed sollte eigentlich !todo.completed lauten, was die logische Negation darstellt – andernfalls kompiliert der Code nicht.

type Action =
  | { type: 'ADD_TODO'; payload: string }
  | { type: 'TOGGLE_TODO'; payload: string };

function todoReducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: crypto.randomUUID(), text: action.payload, completed: false }];
    case 'TOGGLE_TODO':
      return state.map((todo) =>
        todo.id === action.payload ? { ...todo, completed: ...todo.completed } : todo
      );
    default:
      return state;
  }
}

Reducer bringen in zweierlei Hinsicht Vorteile:

  • Testbarkeit. Ein Reducer ist eine reine Funktion, sodass man ihn testen kann, ohne etwas rendern oder React simulieren zu müssen.
  • Vorhersehbarkeit. Jede Aktualisierung erfolgt über eine benannte Aktion anstelle von willkürlichen Settern, die aus verstreuten Callbacks aufgerufen werden – dadurch lassen sich Änderungen leicht nachverfolgen und protokollieren.

4. Keine Tests, weil „Testing ein großes Projekt ist“

Viele Teams betrachten Testing als Entweder-oder: entweder eine umfassende End-to-End-Einrichtung, die überwältigend wirkt, oder gar nichts. Ein besseres Vorgehen beginnt mit den Tests, die am einfachsten zu schreiben und am wertvollsten sind, beizubehalten.

Priorisieren Sie in dieser Reihenfolge:

  • Selbstständige Geschäftslogik. Hilfsfunktionen, Formatierer, Validatoren und Utility-Funktionen, die außerhalb des Komponentenbaums liegen.
  • Reducers und benutzerdefinierte Hooks. Reine Logik zur Zustandsänderung.
  • Kritische Benutzerprozesse. Die wichtigsten Interaktionen, getestet mit React Testing Library (@testing-library/react).
  • Ein Reducer-Test ist ein guter erster Schritt. Er erstellt einen initialen Zustand, sendet eine Aktion direkt über die Funktion und prüft das Ergebnis – ohne dass etwas gerendert wird:

    // Example: Testing a reducer takes 2 minutes and provides huge peace of mind
    test('should toggle todo status', () => {
      const initialState = [{ id: '1', text: 'Write Article', completed: false }];
      const newState = todoReducer(initialState, { type: 'TOGGLE_TODO', payload: '1' });
    
      expect(newState[0].completed).toBe(true);
    });
    

    Ein solcher Test dauert Minuten, um geschrieben zu werden, und schützt die Regeln, von denen die gesamte Funktion abhängt. Sobald der Tippfehler im Reducer aus dem vorherigen Abschnitt behoben ist, wäre genau dieser Test derjenige gewesen, der ihn aufgefangen hätte.

    5. Verwendung von useEffect zur Synchronisierung von React mit sich selbst

    useEffect ist der am häufigsten falsch verwendete Hook in React. Ein gängiges Muster ist seine Verwendung, um den internen Zustand bei einer Änderung eines Props zu kopieren oder anzupassen – was zu einer zusätzlichen Renderung und einem kurzen Moment der inkonsistenten Benutzeroberfläche führt.

    Zwei Missverständnisse tauchen immer wieder auf:

    • „Der Zustand muss in einem Effect aktualisiert werden, wenn sich eine Eigenschaft ändert.“ In der Regel nicht. Berechnen Sie den Wert während des Renderns – oder geben Sie einem Kind, dessen Zustand neu initialisiert werden soll, einen key, der sich mit der entsprechenden Eigenschaft ändert, damit React es neu einbindet.
    • „Klickverhalten gehört in einen Effect.“ Logik, die durch eine Benutzeraktion ausgelöst wird, gehört in den Ereignishandler wie onClick, nicht in einen Effect, der die durch den Klick verursachte Zustandsänderung überwacht.

    Das Leitprinzip: Effects dienen als Ausweg, um mit Systemen außerhalb von React zu synchronisieren – beispielsweise Browser-APIs, Abonnements, Analysetools oder DOM-Widgets von Drittanbietern. Daten, die React bereits besitzt, können ohne Effects umgewandelt werden. Für eine ausführlichere Darstellung mit Beispielen vor und nach der Anwendung siehe Stoppen des Synchronisierens von State mit useEffect.

    6. Standardmäßig alles memorisieren

    Leistungsoptimierungen neigen dazu, zwischen zwei Extremen zu schwanken: Entweder werden die Renderkosten völlig ignoriert, oder jeder Komponente wird mit React.memo und jede Funktion mit useCallback umhüllt.

    Warum eine pauschale Memorisierung kontraproduktiv ist

    useMemo und useCallback sind nicht kostenlos. React muss die Abhängigkeiten speichern und sie bei jeder Renderung vergleichen. Bei einer Komponente, deren Renderierung kostengünstig ist, kann diese Verwaltung genauso viel Zeit in Anspruch nehmen wie die eigentlich zu vermeidende Renderung, und außerdem erschwert sie das Verständnis des Codes. Die Memoisierung hilft zudem nur, wenn alle Eigenschaften stabil sind; ein inlined Objekt oder eine Arrow-Funktion, die an ein gememoisiertes Kind übergeben wird, macht sie wirkungslos.

    Ein bewussterer Ansatz

    • Profilieren Sie zuerst. Der Profiler in den React DevTools zeigt an, welche Komponenten tatsächlich zu oft oder zu langsam gerendert werden.
    • Bessern Sie zunächst die Struktur, bevor Sie Memoisierung einsetzen. Das Verschieben des Zustands in die Komponente, die ihn verwendet, oder das Übergeben teurer Unterbäume als children verhindert oft unnötige Renderungen – ohne jegliche Memoisierung.
  • Memoisieren, wo es wichtig ist. Wenden Sie React.memo auf große Listen und ressourcenintensive Diagramme an und halten Sie ihre Props mit useCallback sowie useMemo stabil.
  • 7. CSS und Barrierefreiheit als Aufgabe anderer betrachten

    Schlagkräftige React-Entwickler sind auch in der Browserplattform kompetent. Komponenten sind nur so gut wie die Markup-Strukturen und Styles, die sie erzeugen.

    Bereiche, die man beherrschen sollte:

    • Moderne Layouts. Kenntnisse in Flexbox und CSS Grid sollten ausreichen, sodass absolute Positionierung nicht mehr die Notlösung für gewöhnliche Layouts ist.
    • Begrenztes Styling. Halten Sie Styles direkt bei den Komponenten mit CSS Modules, Tailwind CSS oder CSS-in-JS bereit, anstatt auf große globale Stylesheets angewiesen zu sein, in denen Klassennamen kollidieren.
  • Bereichbarkeit und Benutzerfreundlichkeit. Verwenden Sie semantische Elemente, wie einen echten <button> anstelle von <div onClick={...}>. Stellen Sie sicher, dass die Tastaturnavigation mit Tab, Enter und Space überall funktioniert. Geben Sie sichtbare :focus-visible-Stile sowie klare interaktive Zustände an, wie beispielsweise einen Zeiger auf klickbaren Elementen.
  • Überprüfungs-Checkliste

    Beim Überprüfen eines React-Pull Requests achten Sie auf folgende Ersatzlösungen:

    • Ausgeleitete Werte, die in zusätzlichen useState-Hooks gespeichert werden: Berechnen Sie sie stattdessen während des Renderings.
    • Manuelle API-Aufrufe innerhalb von useEffect: Verwenden Sie TanStack Query, SWR oder RTK Query.
    • Viele useState-Aufrufe für komplexe Arrays und Objekte: Verlegen Sie die Transitions in useReducer und testen Sie den Reducer.
  • Keine Tests, da die Einrichtung teuer erscheint: Beginnen Sie mit reinen Funktionen, Reduzern und benutzerdefinierten Hooks.
  • useEffect wird zur Synchronisierung des Zustands verwendet: Erstellen Sie den Wert oder handhaben Sie ihn im Ereignishandler.
  • useCallback und React.memo überall einsetzen: Analysieren Sie zuerst, bevor Sie speichern.
  • Klickbare div-Elemente und fehlende Fokustypografien: Verwenden Sie semantisches HTML und überprüfen Sie die Tastaturzugänglichkeit.
  • Zusammenfassung

    Die meisten dieser Probleme haben einen gemeinsamen Ursprung: Zustände oder Logiken, die an zu vielen Stellen vorhanden sind oder häufiger ausgeführt werden, als nötig. Durch die Beibehaltung einer einzigen Quelle der Wahrheit, das Überlassen von Serverdaten an speziell dafür entwickelte Tools, die Isolierung von Übergängen in reinen Funktionen sowie das Reservieren von Effekten für die Außenwelt wird die Anzahl der beteiligten Komponenten verringert. Code mit weniger solchen Komponenten lässt sich leichter testen, bei Bedarf einfacher optimieren und fällt für den nächsten Prüfer leichter ins Vertrauen.