alle Artikel
4 Minuten

useCallback() verstehen und anwenden

Frank Lechner & KI ·
„Dieser Artikel zeigt die drei Kontexte, in denen useCallback wirklich etwas bringt, und wann ihr ihn getrost weglassen solltet.”
useCallback() verstehen und anwenden
Dieser Artikel wurde zuletzt am bearbeitet!
Dieser Text wurde mit Hilfe von KI erstellt.

useCallback() memoisiert eine Funktionsreferenz zwischen Re-Renders, solange sich die Dependencies nicht ändern. Im Kern ist es useMemo(() => fn, deps) für Funktionen. Wann ist es wichtig, dass eine Funktion über mehrere Renderings dieselbe Referenz behält? Spoileralarm! Hier gilt weitgehend das gleiche wie für useMemo().

1. Das Grundproblem: Referenzielle Gleichheit

Bei JavaScript muss man zwischen Wertgleichheit und Referenzgleichheit unterscheiden. Bei primitiven Werten wie Strings, Zahlen oder Boolean ist das relativ einfach:

const a = 42;
const b = 42;

console.log(a === b); // true

Bei Objekten, Arrays und Funktionen sieht es anders aus:

const a = { name: "Frank" };
const b = { name: "Frank" };

console.log(a === b); // false

Obwohl die beiden Objekte denselben Inhalt haben, sind sie zwei verschiedene Objekte.

Man kann sich vorstellen:

a ───────► { name: "Frank" }
           
b ───────► { name: "Frank" }

a und b zeigen auf unterschiedliche Objekte im Speicher.

Deshalb:

a === b // false

Derselbe Objektverweis

Wenn du dagegen ein Objekt kopierst, ohne ein neues Objekt zu erzeugen:

const a = { name: "Frank" };
const b = a;

console.log(a === b); // true

Jetzt:

a ─────┐
       ├──► { name: "Frank" }
b ─────┘

Beide Variablen referenzieren dasselbe Objekt.

Das ist referenzielle Gleichheit.

Funktionen und Arrays sind in JavaScript ebenfalls Objekte und unterliegen deshalb demselben Prinzip.

React verwendet an vielen Stellen referenzielle Vergleiche (Shallow Comparison), statt Objekte tief miteinander zu vergleichen (Deep Comparison). Das ist eine wichtige Performanceentscheidung.

Jeder Re-Render erzeugt eine neue Referenz obwohl die Werte oder die Funktionslogik gleich geblieben sind. Das kann zu einigen Problemen führen, die nachfolgend besprochen und gelöst werden. Das Lösungsprinzip ist dabei immer gleich: Wir stabilisieren die Referenz.

2. Funktionen als Props für Kind-Komponenten mit React.memo()

Für eine Funktion als Prop ohne useCallback erzeugt jede Elternkomponente bei jedem Render eine neue Funktionsreferenz – React.memo() an der Kind-Komponente bringt dann nichts, weil die Prop-Referenz sich "ändert", obwohl die Logik gleich bleibt. Die Kind-Komponente würde erneut rendern weil sich die Props geändert haben. Das ist ein klassischer Fall für useCallback. Hier ein Beispiel um dies zu verhindern:

const handleClick = useCallback(() => {
  doSomething(id);
}, [id]);

<MemoizedButton onClick={handleClick} /> // memoisiert mit React.memo()

3. Funktionen als Dependencies von Hooks

3.1. Als Dependency in useEffect

Wenn eine Funktion innerhalb eines useEffect verwendet wird und selbst eine Dependency ist, verhindert useCallback, dass der Effect bei jedem Render erneut ausgelöst wird. Im folgenden Beispiel werden dadurch unnötige Fetches vermieden:

const fetchData = useCallback(() => {
  api.get(`/items/${id}`);
}, [id]);

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

3.2. Als Dependency in useMemo

Analog dazu: Wenn eine Berechnung in useMemo von einer Callback-Funktion abhängt.

function Calculator({ value }) {
  const [multiplier, setMultiplier] = useState(2);

  // Stabile Funktion, ändert sich nur wenn multiplier sich ändert
  const multiply = useCallback(
    (num) => num * multiplier,
    [multiplier]
  );

  // Nutzt multiply als Dependency
  const result = useMemo(() => {
    console.log('berechne neu...');
    return multiply(value);
  }, [value, multiply]);

  return <div>Ergebnis: {result}</div>;
}

Einfacher wäre es, die Funktion außerhalb der Komponente zu halten, zum Beispiel im Verzeichnis /libs. Das ist aber nicht immer möglich, zum Beispiel wenn die Funktion Props oder State der Komponente als Argumente benötigt.

3.3. Als Dependency in Custom Hooks

Ein Custom Hook mit Funktion als Dependency:

// Custom Hook, der eine Callback-Funktion als Parameter bekommt
function useInterval(callback, delay) {
  useEffect(() => {
    const id = setInterval(callback, delay);
    return () => clearInterval(id);
  }, [callback, delay]); // callback ist Dependency
}

Verwendung – callback muss stabilisiert werden:

function Timer() {
  const [count, setCount] = useState(0);

  const tick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // stabil, da setCount garantiert stabil ist

  useInterval(tick, 1000);

  return <div>Sekunden: {count}</div>;
}

Ohne useCallback bei tick würde bei jedem Render von Timer eine neue Funktionsreferenz entstehen. Das useEffect innerhalb von useInterval sähe dann bei jedem Render eine "geänderte" Dependency (callback) und würde das Interval ständig löschen und neu erstellen – im schlimmsten Fall tickt der Timer nie richtig, weil er permanent zurückgesetzt wird.

3.4. In Custom Hooks, die Funktionen zurückgeben

Hier ein sehr reduziertes Beispiel, das useCallback innerhalb eines Custom Hooks zeigt:

function useCounter() {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return { count, increment };
}

Verwendung:

function Counter() {
  const { count, increment } = useCounter();

  return (
    <div>
      <p>Zähler: {count}</p>
      <button onClick={increment}>+</button>
    </div>
  );
}

Wenn ein Custom Hook Funktionen exportiert, sollten diese stabil sein, damit Consumer des Hooks nicht unnötig re-rendern oder Effects triggern.

4. Zum vermeiden von Re-Render-Kaskaden bei Context

Relevant ist useCallback() auch in Bezug auf die Context – und zwar in einem sehr ähnlichen Fall wie bei useMemo und Context. Wenn der Context-Value Funktionen enthält, sorgt useCallback dafür, dass nicht alle Consumer unnötig neu rendern.

4.1. Das Problem ohne useCallback

function AppProvider({ children }) {
  const [user, setUser] = useState(null);

  // Neue Funktionsreferenz bei JEDEM Render von AppProvider
  const login = (credentials) => {
    // ... login logic
    setUser(credentials);
  };

  const value = { user, login };

  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  );
}

Jeder Consumer, der useContext(AppContext) nutzt, rendert bei jedem Render von AppProvider neu – selbst wenn user sich gar nicht geändert hat. Grund: value ist ein neues Objekt, und login ist eine neue Funktion, bei jedem einzelnen Render.

4.2. Die Lösung: useCallback + useMemo kombiniert

function AppProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = useCallback((credentials) => {
    setUser(credentials);
  }, []); // stabil, da setUser garantiert stabil ist

  const value = useMemo(
    () => ({ user, login }),
    [user, login]
  );

  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  );
}

Warum beide Hooks nötig sind:

  • useCallback stabilisiert login selbst

  • useMemo stabilisiert das value-Objekt, das login enthält

Ohne useCallback würde login bei jedem Render neu erzeugt → useMemo sähe eine "geänderte" Dependency → value würde trotzdem neu erstellt → der ganze useMemo-Aufwand wäre umsonst.

5. Alternativen zu useCallback

useCallback ist oft nicht die einzige oder die beste Lösung. Hier die Alternativen, geordnet danach, wie oft sie in der Praxis tatsächlich sinnvoll sind:

5.1. Funktion aus der Komponente rausziehen

Wenn die Funktion keine Props/State aus der Komponente braucht, muss sie gar nicht innerhalb der Komponente definiert werden – dann ist sie automatisch stabil, ganz ohne Hook.

5.2. useState mit Updater-Funktion statt Closure über State

Oft braucht man useCallback nur, weil eine Funktion von state abhängt. Mit der Updater-Form (c => c + 1) entfällt die Dependency komplett.

// Braucht [count] als Dependency
const increment = useCallback(() => setCount(count + 1), [count]);

// Keine Dependency nötig, count wird nie "gebraucht"
const increment = useCallback(() => setCount(c => c + 1), []);

5.3. useReducer statt mehrerer Callbacks

Wenn du viele zusammengehörige State-Updates + Callbacks hast, wird dispatch von React garantiert stabil geliefert – kein useCallback nötig.

function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  // dispatch ist stabil, kein useCallback nötig für die "Handler"
  return <button onClick={() => dispatch({ type: 'increment' })}>+</button>;
}

5.4. useRef für Werte, die keine Re-Renders auslösen sollen

Wenn eine "Funktion" eigentlich nur einen aktuellen Wert referenzieren soll (z. B. bei Event-Listenern, Timern), ist useRef oft passender als useCallback mit vielen Dependencies.

function useLatestCallback(callback) {
  const ref = useRef(callback);
  useEffect(() => { ref.current = callback; });
  return useCallback((...args) => ref.current(...args), []);
}

5.5. React Compiler ab React 19.2 und ab Next.js 16

Der größte strukturelle Umbruch: Der React Compiler (früher "React Forget") analysiert deinen Code React 19.2 (Seit Oktober 2025) statisch ab und fügt Memoisierung automatisch ein – du schreibst gar kein useCallback/useMemo mehr manuell.

// Mit React Compiler: kein useCallback nötig,
// der Compiler memoisiert automatisch, wo sinnvoll
function Counter() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(c => c + 1);
  return <button onClick={increment}>+</button>;
}

5.6. Einfach nichts tun

Die unterschätzteste Alternative: Wenn keine der drei relevanten Situationen vorliegt (React.memo-Kind, useEffect/useMemo-Dependency, Custom-Hook-Rückgabewert), ist eine neue Funktion pro Render schlicht kein Problem. Der Vergleichsaufwand von useCallback selbst kostet mehr, als er einspart.

Faustregel: Bevor du zu useCallback greifst, prüfe erst 1–4 als strukturelle Lösung. useCallback ist eher das Werkzeug für den Fall, dass die Funktion wirklich von Props/State abhängt UND in einem der drei relevanten Kontexte landet.

6. Fazit

Der Vergleich der Dependency-Arrays und das Vorhalten der alten Funktion kosten selbst auch etwas. useCallback lohnt sich konkret bei:

  • Props an React.memo-Komponenten

  • Dependencies in useEffect/useMemo

  • Return-Werten aus Custom Hooks

Ohne einen dieser drei Kontexte ist useCallback in der Regel reiner Overhead ohne Nutzen – eine neue Funktion bei jedem Render zu erzeugen ist für sich genommen extrem billig.

Das mentale Modell:

                        useCallback()
                              │
                              ▼
                      stabile Referenz
                              │
              ┌───────────────┼────────────────┬
              │               │                │               
              ▼               ▼                ▼               
        React.memo()        Hooks           Context  
                              │
                              ▼
                      1. fn als Dependency
                     2. fn als Rückgabewert

Hooks sind hier einerseits fn als Dependencies und andereseits custom Hooks die funktionen zurück geben.

Die wichtigsten Merksätze

useMemo() → „Merke dir diesen Wert.“

useCallback() → „Merke dir diese Funktion.“

React.memo() → „Überspringe das Rendering, wenn die Props gleich geblieben sind.“