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:
filterundsortlaufen nur erneut, wenn sichordersoderkeywordändern.- Wenn du auf “Design wechseln” klickst, ändert sich
darkModeund die Komponente rendert neu. Diesmal gibtuseMemodas gespeicherte Array zurück, ohne erneut zu filtern. sortist hier sicher, dafilterbereits ein neues Array zurückgibt. Keine Operation verändert direkt das Proporders.
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. VerwendeuseMemonur, 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:
handleSelecthat ein leeres dependency array, dasetSelectedIdimmer stabil ist (React garantiert das füruseStateSetter).- Wenn du in das Notiz-Feld eingibst, rendert sich
OrderListneu. Aber jedeOrderRowerhält das gleicheorder, den gleichenhandleSelect, den gleichenonRender, also überspringtmemosie. - Wenn du
handleSelectzu 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
memoweitergegeben. - Kind-Komponente erhält sowieso Props, die sich jedes Mal ändern. Zum Beispiel
<OrderRow order={{ ...o }} />oder übergibchildrenals JSX. JSX ist auch ein neues Objekt bei jedem Render, also wirdmemoimmer Unterschiede sehen. - Struktur ändern statt zu memoizen. State näher zur Nutzung bringen oder teure Komponenten über
childrenweitergeben 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 inuseMemo. Sie gehören zuuseEffectoder 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:
- Eine messbar teure Berechnung.
- Ein Wert oder eine Funktion, die an eine Komponente mit
React.memoübergeben wird. - Ein Wert oder eine Funktion, die in einem
useEffectoder 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.