Startseite / Artikel

Die richtige React-Tool-Wahl: Derive, Handle, Fetch, Defer oder Effect

Ein Entscheidungshilfeleitfaden zur Ersetzung von reflexiven useEffect-Aufrufen durch abgeleitete Werte, Ereignishandler, eine Datenschicht, useTransition, measured useMemo sowie React 19-APIs.

Verlag: REACTAPP.TOP8 Min. Lesezeit1896 Wörter

Quellen: Stop Using `useEffect` for Everything: 7 React Patterns That Make Your Code Simpler

Viele React-Komponenten sammeln useEffect-Aufrufe zur Datensuche, Reaktion auf Props, Berechnung von Werten sowie zum Aufrechterhalten der Zustandsübereinstimmung. Einige Monate später öffnet man die Datei und findet vier Effects, drei abgeleitete Zustände, zwei Ladeindikatoren sowie einen Fehler, den niemand erklären kann. Dieser Leitfaden gibt Ihnen eine einzige Frage, die Sie vor dem Schreiben eines Hooks stellen sollten: „Braucht das wirklich einen Effect?“, sowie sieben konkrete Muster dafür, was man tun soll, wenn die Antwort nein lautet.

Ableitete Werte gehören in render, nicht in den Zustand

Die häufigste Fehlnutzung besteht darin, etwas zu speichern, was berechnet werden kann. Angenommen, Sie zeigen Produkte an, die nach einer Suchanfrage gefiltert wurden. Ein gängiger erster Versuch besteht darin, eine Zustandsvariable für das Ergebnis hinzuzufügen:

const [filteredProducts, setFilteredProducts] = useState([]);

und anschließend einen Effect, der die Berechnung jedes Mal wiederholt, wenn sich die Eingaben ändern:

useEffect(() => {
  setFilteredProducts(
    products.filter(product =>
      product.name.toLowerCase().includes(query.toLowerCase())
    )
  );
}, [products, query]);

Das funktioniert, aber filteredProducts ist kein unabhängiger Zustand. Es handelt sich dabei um eine reine Funktion der bereits vorhandenen Werte:

products
+
query
=
filteredProducts

Durch Speichern davon rendernt React zunächst veraltete Ergebnisse, führt anschließend den Effect aus und rendernt erneut. Berechnen Sie es stattdessen direkt im Komponentenkörper:

const filteredProducts = products.filter(product =>
  product.name.toLowerCase().includes(query.toLowerCase())
);

Am Ende haben Sie weder eine zusätzliche Zustandsvariable noch einen Effect, nichts, was synchronisiert werden muss, und auch keinen zweiten Render nur zur Erzeugung des Ergebnisses. Wenn das Filtern tatsächlich aufwändig ist – beispielsweise bei einer sehr großen Liste – umschließen Sie dieselbe Ausdrucksformel mit useMemo:

const filteredProducts = useMemo(() => {
  return products.filter(product =>
    product.name.toLowerCase().includes(query.toLowerCase())
  );
}, [products, query]);

Das Memoisieren ist ein Detail. Entscheidend ist das Prinzip: Ein aus Props oder Zustand abgeleiteter Wert benötigt keinen Platz im Zustand.

Benutzeraktionen gehören in Event-Handler

Ein anderes Muster leitet einen Klick über den Zustand weiter, damit ein Effect darauf reagieren kann. Zuerst eine Flagge:

const [shouldSave, setShouldSave] = useState(false);

Dann ein Effect, der die Flagge überwacht, sowie ein Handler, der sie setzt:

useEffect(() => {
  if (shouldSave) {
    saveUser();
    setShouldSave(false);
  }
}, [shouldSave]);function handleSave() {
  setShouldSave(true);
}

Es gibt keinen Grund für diese Umwege. Der Klick ist der Moment, in dem die Aktion stattfinden sollte, und Sie haben bereits eine Funktion, die zu diesem Zeitpunkt ausgeführt wird:

function handleSave() {
  saveUser();
}

Vergleichen Sie die beiden Abläufe. Die Version mit der Flagge durchläuft mehrere Schritte, bevor etwas gespeichert wird, und löst anschließend eine weitere Renderung aus, um sich selbst zurückzusetzen:

Click
  ↓
State update
  ↓
Render
  ↓
Effect
  ↓
saveUser()
  ↓
Another state update
  ↓
Render

Die direkte Version umfasst nur einen Schritt:

Click
  ↓
saveUser()

Die Faustregel lautet, dass Ereignisse in Event-Handlern abgehandelt werden und Effects dazu dienen, mit Systemen außerhalb von React zu synchronisieren – nicht, um gewöhnliche Ereignislogik zu verbergen.

Kopieren Sie Eigenschaften in den Zustand nur dann, wenn Sie zwei Werte benötigen

Dieser Ansatz scheint unbedenklich zu sein. Eine Komponente behält eine eigene Kopie eines Namens bei:

const [name, setName] = useState('');

und ein Effect überschreibt diese Kopie jedes Mal, wenn sich die Eigenschaft ändert:

useEffect(() => {
  setName(user.name);
}, [user]);

Jetzt gibt es zwei Quellen der Wahrheit, und zu jedem Zeitpunkt muss man prüfen, welche davon korrekt ist:

user.name
     +
name

Falls man lediglich den Wert anzeigt, indem man ihn direkt aus der Eigenschaft liest:

return <h2>{user.name}</h2>;

Es gibt eine berechtigte Ausnahme: Ein Bearbeitungsformular, das absichtlich eine vorübergehende, bearbeitbare Kopie der Serverdaten speichert, bis der Benutzer sie speichert oder abbricht:

Server data
    ↓
Edit form
    ↓
User changes fields
    ↓
Save / Cancel

Hier ist der lokale Zustand korrekt, weil das Entwurfsobjekt tatsächlich einen eigenen Wert mit eigenem Lebenszyklus darstellt. Die Lektion lautet nicht „kopiere niemals Props in den Zustand“, sondern „verstehe, warum du eine zweite Quelle der Wahrheit erstellst“. Im Fall von Formularen ist es oft sauberer, den Entwurfszustand durch Zuweisen einer key, die mit der ID des Datensatzes verknüpft ist, zurückzusetzen, anstatt mit einem Effect zu synchronisieren; unser Artikel über den Ersatz von Zustands-Synchronisierungs-Effects erläutert diese Technik ausführlich.

Lass die Datenlage das Abrufen übernehmen

Das Laden von Daten verteilt sich oft auf mehrere Effects. Es beginnt mit einer Anfrage beim Initialisieren:

useEffect(() => {
  fetchUser();
}, []);

und entwickelt sich zu einer Reihe von Effects, die jeweils etwas für denselben Benutzer abrufen:

useEffect(() => {
  fetchOrders();
}, [userId]);useEffect(() => {
  fetchNotifications();
}, [userId]);useEffect(() => {
  fetchPermissions();
}, [userId]);

Zu diesem Zeitpunkt ist die Komponente zu einer API-Orchestrierungsschicht geworden. In größeren Anwendungen lohnt sich hier eine spezielle Lösung für das Abrufen von Daten. Je nach verwendetem Stack könnte das sein:

  • TanStack Query
  • SWR
  • die Datenabruffunktionen Ihres Frameworks
  • Server Components, sofern sie geeignet sind

Andernfalls müssen Sie für jede Anfrage alle diese Aspekte manuell handhaben:

loading
error
data
refetch
cache
retry
stale data
race conditions

Eine Daten-Schicht übernimmt das Ganze für Sie. Konzeptuell deklariert eine Abfrage, welche Daten sie benötigt und wie diese beschafft werden sollen, sowie den aktuellen Status:

const { data, isLoading, error } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId)
});

Der Abfrageschlüssel dient gleichzeitig als Cache-Schlüssel, sodass Komponenten, die nach demselben Benutzer fragen, denselben Anfragevorgang teilen. Durch Änderung von userId wird der neue Benutzer abgerufen, ohne dass veraltete Antworten Vorrang haben. Die Komponente kehrt wieder zur Darstellung der Benutzeroberfläche zurück, anstatt als kleine Netzwerkbibliothek zu fungieren.

Nicht dringende Aktualisierungen mit useTransition markieren

Betrachten Sie ein Suchfeld, das eine ressourcenintensive Tabelle ansteuert. Bei einer einzigen Zustandsaktualisierung zwingt jede Tasteneingabe die Tabelle dazu, neu gerendert zu werden, bevor der Eingabebereich den nächsten Zeichen anzeigt:

function handleChange(e) {
  setQuery(e.target.value);
}

Die Lösung besteht darin, den Wert in zwei Teile aufzuteilen: Was im Eingabebereich angezeigt wird, und woran sich die Tabelle filtert:

const [inputValue, setInputValue] = useState('');
const [query, setQuery] = useState('');

Der Eingabewert wird normal aktualisiert, während die Abfragenaktualisierung in startTransition eingebettet wird:

const [isPending, startTransition] = useTransition();function handleChange(e) {
  const value = e.target.value;  setInputValue(value);  startTransition(() => {
    setQuery(value);
  });
}

React behandelt nun die beiden Aktualisierungen unterschiedlich. Das Eintippen gilt als dringend:

Typing
↓
Urgent

während das Filtern und Neurendern unterbrochen und verzögert werden können:

Filtering/rendering
↓
Can wait

Eine Übergangslogik macht langsamen Code nicht schneller; sie ändert lediglich die Scheduling-Strategie, damit die Benutzeroberfläche weiterhin reaktiv bleibt und eine neue Tasteneingabe einen laufenden Render unterbrechen kann. isPending ermöglicht es, anzuzeigen, dass die Tabelle aufholt.

Messen, bevor man speichert

Der gegenteilige Fehler ist die reflexhafte Optimierung. Oft beginnt sie mit einer einmal berechneten und gespeicherten Werte:

const result = useMemo(() => {
  return calculateSomething(data);
}, [data]);

folgt von einer gespeicherten Zuordnung:

const users = useMemo(() => {
  return data.map(...);
}, [data]);

dann von einer gespeicherten Spaltendefinition:

const columns = useMemo(() => [...], []);

und gespeicherten Optionen:

const options = useMemo(() => [...], []);

Bald ist jeder Wert in der Komponente abgepackt, doch niemand hat jemals überprüft, ob etwas langsam war. useMemo hilft bei aufwändigen Berechnungen und stellt stabile Referenzen für gespeicherte Kinderkomponenten bereit, doch die Optimierung sollte einer Schleife folgen:

Problem
  ↓
Measure
  ↓
Find bottleneck
  ↓
Optimize
  ↓
Measure again

und nicht einem Abkürzungsweg:

Component feels complicated
  ↓
Add useMemo

Bei günstigen Berechnungen führt Memoisierung nur zu zusätzlichem Lesenaufwand sowie Abhängigkeitsarrays, die veraltet werden können. Der React DevTools Profiler zeigt an, ob ein Komponent tatsächlich das Engpassproblem darstellt.

Nutzen Sie die von React 19 hinzugefügten Primitiven

Mehrere Gewohnheiten aus früheren Versionen von React haben nun einfachere Ersatzlösungen. Über Jahre hinweg bedeutete das Übergeben eines refs an eine eigene Komponente, diese in forwardRef zu verpacken. In React 19 erhalten Funktionskomponenten ref als gewöhnliches Prop:

function CustomInput({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

Optimistische UIs sind ein weiteres Beispiel. Anstatt manuell einen temporären Eintrag in den Zustand einzufügen und sich daran zu erinnern, ihn später zu entfernen oder zu ersetzen:

setTodos(prev => [
  ...prev,
  {
    id: 'temp',
    text,
    pending: true
  }
]);

kann man useOptimistic verwenden, das eine temporäre Ansicht aus dem tatsächlichen Zustand ableitet:

const [optimisticTodos, addOptimisticTodo] = useOptimistic(
  todos,
  (state, todo) => [...state, todo]
);

Der optimistische Wert wird angezeigt, solange eine asynchrone Aktion im Gange ist, und wechselt anschließend zum tatsächlichen todos, sobald die Aktion abgeschlossen ist – es gibt somit keinen vorübergehenden Eintrag, den man entfernen müsste. Er soll innerhalb einer Action oder eines Übergangs aufgerufen werden; die Randfälle bei fehlgeschlagenen Anfragen werden in unserem Artikel zu den Fehlermodi von useOptimistic rollback behandelt.

Für IDs, die Beschriftungen mit Eingabefeldern zur Barrierefreiheit verbinden, verwenden Sie den integrierten Hook:

const id = useId();

anstatt eines zufällig generierten IDs:

const id = `input-${Math.random()}`;

Ein zufälliger ID ändert sich bei jeder Neuaufladung und unterscheidet sich zwischen Server und Client, was die Hydratierung sowie die Verknüpfung der Beschriftungen stört. React fügt kontinuierlich Primitive hinzu, die Absichten direkt ausdrücken – vermeiden Sie daher, aktuelle Probleme mit Mustern zu lösen, die bereits vor ihnen existierten.

Checkliste vor dem Hinzufügen eines Hooks

Eine gute Kenntnis von React beinhaltet auch das Wissen, wann Hooks weggelassen werden sollten. Ein Komponenten wird nicht besser, nur weil alle diese Hooks verwendet werden:

useEffect()
useMemo()
useCallback()
useRef()
useState()

Zielen Sie auf einen leicht verständlichen Datenfluss ab. Bevor Sie zu einem Hook greifen, benennen Sie das Problem und wählen Sie das passende Werkzeug dafür aus.

Für abgeleitete Daten:

Calculate it.

Für eine Benutzeraktion:

Handle the event.

Für die Synchronisierung mit etwas außerhalb von React, wie z. B. einer Abonnementfunktion, einem Timer oder einem nicht-React-basierten Widget:

Consider useEffect.

Für das Lesen aus einer externen Datenquelle:

Consider useSyncExternalStore or a state library.

Für eine Aktualisierung, die wichtig ist, aber nicht dringend ist:

Consider useTransition.

Für eine Berechnung, die möglicherweise aufwändig ist:

Measure first, then consider useMemo.

Und wenn React bereits ein primitives Werkzeug für diese Aufgabe bereitstellt:

Use the primitive instead of rebuilding it yourself.

Wichtige Erkenntnisse

  • Die gesündesten Komponenten weisen in der Regel die wenigsten unnötigen Hooks auf, nicht die meisten.
  • Ersetzen Sie „Wo kann ich useEffect verwenden?“ durch „Braucht das wirklich einen Effect?“ Meistens weist die Antwort auf Berechnungen zur Darstellung, Event-Handler oder Datenbibliotheken hin.
  • Lernen Sie zunächst das Problem, das jede neue React-API löst, bevor Sie sich mit ihrer Syntax beschäftigen.
  • Mannchmal ist die effektivste Optimierung in einer Komponente das Löschen eines Hooks anstelle des Hinzufügens eines neuen.