useMemo und useCallback in React: Wann brauchst du sie wirklich?

1. Überblick

useMemo und useCallback werden oft impulsiv eingesetzt: Entweder packst du alles ein “um sicherzugehen”, oder du verwendest sie nie. Beide Ansätze sind falsch. Dieser Artikel erklärt den Mechanismus hinter beiden Hooks, zeigt drei Szenarien, in denen sie wirklich nützlich sind, und Fälle, die du überspringen solltest. Du wirst auch einen Test schreiben, um zu beweisen, dass Memoization tatsächlich funktioniert.

2. Das Kernproblem: Bei jedem Render wird alles neu erstellt

Jedes Mal, wenn eine Komponente neu rendert, läuft die gesamte Funktionsbody von vorne ab. Jedes Objekt, Array und jede Funktion, die darin deklariert ist, wird neu erstellt.

function Checkout({ customer }) {
  const shipping = { city: customer.city, express: true };
  const handlePay = () => console.log('Bezahlung für', customer.name);

  return <PaymentForm shipping={shipping} onPay={handlePay} />;
}

Nach jedem Render von Checkout sind shipping und handlePay neue Referenzen, auch wenn der Inhalt gleich ist. JavaScript vergleicht Objekte und Funktionen nach Referenz:

{ city: 'Huế' } === { city: 'Huế' }   // false
(() => 1) === (() => 1)               // false

Normalerweise ist das harmlos, da Objekte sehr billig zu erstellen sind. Es wird nur problematisch, wenn etwas Referenzen vergleicht. Das könnte React.memo, das dependency array von useEffect oder ein anderer Hook sein. useMemo und useCallback wurden entwickelt, um Referenzen über mehrere Renders hinweg stabil zu halten.

3. useMemo: Einen Wert merken

useMemo(calculate, deps) ruft calculate beim ersten Render auf und speichert das Ergebnis. Bei nachfolgenden Renders rechnet React nur nach, wenn sich ein Element in deps ändert, verglichen mit Object.is.

3.1. Teure Berechnungen pro Render vermeiden

import { useMemo, useState } from 'react';

function OrderReport({ orders }) {
  const [keyword, setKeyword] = useState('');
  const [darkMode, setDarkMode] = useState(false);

  const visibleOrders = useMemo(() => {
    const kw = keyword.toLowerCase();
    return orders
      .filter((o) => o.customer.toLowerCase().includes(kw))
      .sort((a, b) => b.total - a.total);
  }, [orders, keyword]);

  return (
    <div className={darkMode ? 'dark' : ''}>
      <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
      <button onClick={() => setDarkMode((d) => !d)}>Design wechseln</button>
      <ul>
        {visibleOrders.map((o) => (
          <li key={o.id}>{o.customer}: {o.total}</li>
        ))}
      </ul>
    </div>
  );
}

Erklärung:

  • filter und sort laufen nur erneut, wenn sich orders oder keyword ändern.
  • Wenn du auf “Design wechseln” klickst, ändert sich darkMode und die Komponente rendert neu. Diesmal gibt useMemo das gespeicherte Array zurück, ohne erneut zu filtern.
  • sort ist hier sicher, da filter bereits ein neues Array zurückgibt. Keine Operation verändert direkt das Prop orders.

Woher weißt du, ob eine Berechnung “teuer” ist? Messe statt zu raten:

console.time('Bestellungen filtern');
const result = orders.filter((o) => o.customer.includes(keyword));
console.timeEnd('Bestellungen filtern');
Bestellungen filtern: 0.08ms

Eine praktische Faustregel: Wenn eine Berechnung auf deinem Dev-Rechner unter etwa 1ms dauert, ist Memoization kaum nötig. Denk daran, auf einem Production Build zu messen, und noch besser, CPU Throttling in den DevTools einzuschalten, da Benutzer-Geräte oft langsamer sind als deines.

3.2. Ein Objekt stabil halten zum Weitergeben

Der zweite Fall hängt nicht mit Rechengeschwindigkeit zusammen. Hier brauchst du ein Objekt mit stabiler Referenz zum Abhängigkeit sein:

function CustomerOrders({ customerId, status }) {
  const [orders, setOrders] = useState([]);

  const query = useMemo(() => ({ customerId, status }), [customerId, status]);

  useEffect(() => {
    fetchOrders(query).then(setOrders);
  }, [query]);

  return <OrderTable orders={orders} />;
}

Ohne useMemo wäre query nach jedem Render ein neues Objekt. Der Effect würde sich immer wieder ausführen, und da setOrders einen erneuten Render auslöst, könntest du in einer Endlosschleife von API-Aufrufen landen.

Hinweis: Es gibt in diesem Beispiel noch einen einfacheren Weg. Du erstellst das Objekt innerhalb des Effects und setzt primitive Werte in das dependency array: useEffect(() => { fetchOrders({ customerId, status }).then(setOrders); }, [customerId, status]);. Bevorzuge diesen Ansatz. Verwende useMemo nur, wenn das Objekt außerhalb des Effects existieren muss, zum Beispiel wenn es an eine Komponente weitergegeben wird.

4. useCallback: Eine Funktion merken

useCallback(fn, deps) entspricht useMemo(() => fn, deps). Sie gibt exakt die Funktion zurück, die du übergibst, und behält die Referenz, bis sich deps ändert. useCallback macht die Funktion nicht schneller. Dieser Hook ist nur sinnvoll, wenn die Funktion an einer Stelle verglichen wird.

4.1. Mit React.memo kombinieren

React.memo überspringt den Render einer Kind-Komponente, wenn die Props wie beim letzten Mal sind (flacher Vergleich). Wenn ein Prop eine Funktion ist, die jedes Mal neu erstellt wird, verliert memo seine Wirkung.

import { memo, useCallback, useState } from 'react';

const OrderRow = memo(function OrderRow({ order, onSelect, onRender }) {
  onRender?.(order.id);
  return <li onClick={() => onSelect(order.id)}>{order.customer}</li>;
});

export function OrderList({ orders, onRender }) {
  const [selectedId, setSelectedId] = useState(null);
  const [note, setNote] = useState('');

  const handleSelect = useCallback((id) => setSelectedId(id), []);

  return (
    <>
      <input aria-label="note" value={note} onChange={(e) => setNote(e.target.value)} />
      <ul>
        {orders.map((o) => (
          <OrderRow key={o.id} order={o} onSelect={handleSelect} onRender={onRender} />
        ))}
      </ul>
      <p>Aktuell ausgewählt: {selectedId ?? 'nichts'}</p>
    </>
  );
}

Erklärung:

  • handleSelect hat ein leeres dependency array, da setSelectedId immer stabil ist (React garantiert das für useState Setter).
  • Wenn du in das Notiz-Feld eingibst, rendert sich OrderList neu. Aber jede OrderRow erhält das gleiche order, den gleichen handleSelect, den gleichen onRender, also überspringt memo sie.
  • Wenn du handleSelect zu einer normalen Arrow Function änderst, würde jede Zeile bei jedem Tastenanschlag neu rendern.

Wichtig zu merken: useCallback ohne memo auf der Empfängerseite ist praktisch nutzlos. Einen gememoizten Callback an das <button> Element des DOM zu übergeben spart nichts.

4.2. Funktion als Abhängigkeit

function useOrderPolling(customerId) {
  const [orders, setOrders] = useState([]);

  const refresh = useCallback(async () => {
    const data = await fetchOrders({ customerId });
    setOrders(data);
  }, [customerId]);

  useEffect(() => {
    refresh();
    const id = setInterval(refresh, 10_000);
    return () => clearInterval(id);
  }, [refresh]);

  return { orders, refresh };
}

refresh wird sowohl im Effect verwendet als auch an die Außenseite übergeben. Dank useCallback wird das Interval nur neu erstellt, wenn sich customerId ändert. Jede Komponente, die refresh als Abhängigkeit nutzt, wird nicht unnötig ausgeführt.

Das ist eine Regel, die du beim Schreiben von custom Hooks beachten solltest: Funktionen, die von außen zurückgegeben werden, sollten mit useCallback umhüllt sein. Du kannst nicht vorhersagen, ob derjenige, der den Hook nutzt, ihn als Abhängigkeit verwendet oder an ein memo übergibt.

5. Mit Tests beweisen

Statt zu glauben, dass Memoization “wahrscheinlich funktioniert”, kannst du einen Test dafür schreiben. Installiere Vitest und React Testing Library:

npm install -D vitest jsdom @testing-library/react

Konfiguriere die jsdom Umgebung in vite.config.js:

export default {
  test: { environment: 'jsdom' },
};

Test für OrderList aus Abschnitt 4.1:

import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { OrderList } from './OrderList';

const orders = [
  { id: 1, customer: 'Lan' },
  { id: 2, customer: 'Minh' },
];

describe('OrderList', () => {
  it('rendert OrderRow nicht neu, wenn nur Notiz eingegeben wird', () => {
    const onRender = vi.fn();
    render(<OrderList orders={orders} onRender={onRender} />);
    expect(onRender).toHaveBeenCalledTimes(2);

    fireEvent.change(screen.getByLabelText('note'), { target: { value: 'schnelle Lieferung' } });

    expect(onRender).toHaveBeenCalledTimes(2);
  });

  it('aktualisiert immer noch bei Auswahl einer Bestellung', () => {
    render(<OrderList orders={orders} />);
    fireEvent.click(screen.getByText('Minh'));
    expect(screen.getByText('Aktuell ausgewählt: 2')).toBeTruthy();
  });
});
 ✓ OrderList > rendert OrderRow nicht neu, wenn nur Notiz eingegeben wird
 ✓ OrderList > aktualisiert immer noch bei Auswahl einer Bestellung

Versuche, useCallback zu entfernen und den Test erneut auszuführen. Der erste Test schlägt fehl, statt 2 Aufrufe erhält man 4. Das ist der klare Beweis, dass der Hook seine Arbeit macht. Definiere onRender außerhalb und übergebe die gleiche Referenz, sonst könnte dieses Prop selbst das memo brechen.

Neben Tests solltest du den Profiler Tab der React DevTools mit der “Highlight updates” Option verwenden, um visuell zu sehen, welche Komponenten neu rendern.

6. Wann NICHT verwenden

Memoization ist nicht kostenlos. React muss den alten Wert speichern, das dependency array speichern und jedes Mal vergleichen. Der Code wird auch schwerer zu lesen. Du solltest darauf verzichten in diesen Fällen:

  • Billige Berechnung: ein paar Zahlen addieren, Strings verketten, map über ein paar Dutzend Elemente.
  • Primitive Werte: useMemo(() => a + b, [a, b]) gibt eine Zahl zurück. Zahlen werden nach Wert verglichen, also hilft Memoization nichts.
  • Callback an ein DOM-Element oder an eine Kind-Komponente ohne memo weitergegeben.
  • Kind-Komponente erhält sowieso Props, die sich jedes Mal ändern. Zum Beispiel <OrderRow order={{ ...o }} /> oder übergib children als JSX. JSX ist auch ein neues Objekt bei jedem Render, also wird memo immer Unterschiede sehen.
  • Struktur ändern statt zu memoizen. State näher zur Nutzung bringen oder teure Komponenten über children weitergeben ist oft besser und verhindert überflüssige Renders ganz.

Hinweis: Verlass dich nicht auf useMemo, um die Logik korrekt zu machen. React kann den Cache löschen, zum Beispiel in bestimmten Szenarien mit Suspense oder in der Zukunft. Dein Code muss richtig laufen, unabhängig von Memoization. Dieser Hook ist nur für Leistungsoptimierung. Genauso: Keine Side Effects wie API-Aufrufe oder localStorage-Schreibvorgänge in useMemo. Sie gehören zu useEffect oder Event Handlern.

Der häufigste Fehler ist fehlende Dependencies, was zu stale Closures führt:

const handleSave = useCallback(() => {
  saveOrder({ ...order, note });
}, [order]); // `note` fehlt

handleSave wird immer den note Wert aus der ersten Erstellung verwenden. Aktiviere die Regel react-hooks/exhaustive-deps von eslint-plugin-react-hooks und ignoriere ihre Warnungen nicht.

7. Was ändert sich mit React Compiler?

Der React Compiler, seit Ende 2025 stabil, analysiert Komponenten zur Build-Zeit und fügt automatisch Memoization auf detaillierteren Ebenen ein als du es normalerweise von Hand schreiben würdest. Um es in einem Vite oder Babel Projekt zu versuchen:

npm install -D babel-plugin-react-compiler

Wenn der Compiler aktiviert ist, brauchst du fast nie useMemo, useCallback oder memo zu schreiben. Die alten Hooks funktionieren immer noch. Sie sind nützlich, wenn du exakte Kontrolle über eine Effect-Abhängigkeit brauchst. Der Compiler kann nur Code optimieren, der Reacts Regeln folgt: reine Komponenten, keine direkten Änderungen von Props oder State. Deshalb ist es wichtig, diese beiden Hooks zu verstehen, um alten Code zu lesen und zu debuggen, wenn der Compiler eine Komponente übersieht.

8. Fazit

useMemo und useCallback lösen das gleiche Problem: Referenzen stabil halten über mehrere Renders. Sie sind in drei Situationen wirklich wert, verwendet zu werden:

  1. Eine messbar teure Berechnung.
  2. Ein Wert oder eine Funktion, die an eine Komponente mit React.memo übergeben wird.
  3. Ein Wert oder eine Funktion, die in einem useEffect oder anderen Hook dependency array ist, einschließlich Funktionen, die von einem custom Hook zurückgegeben werden.

Außerhalb dieser Fälle halte deinen Code einfach. Messe mit dem Profiler, beweise mit Tests, und erwäge, React Compiler zu aktivieren, damit das Tool diese Arbeit für dich macht.

reacthooksusememousecallbackperformance