Es ist kein Geheimnis, dass React.js in den letzten Jahren sehr populär geworden ist. Es ist jetzt die JavaScript-Bibliothek der Wahl für viele der bekanntesten Akteure im Internet, darunter Facebook und WhatsApp.

Einer der Hauptgründe für ihren Aufstieg war die Einführung von Hooks in Version 16.8. Mit React Hooks kannst du die Funktionalität von React nutzen, ohne Klassenkomponenten zu schreiben. Jetzt sind funktionale Komponenten mit Hooks die bevorzugte Struktur für Entwickler, die mit React arbeiten.

In diesem Blogbeitrag werden wir einen bestimmten Hook – useCallback – genauer unter die Lupe nehmen, denn er berührt einen grundlegenden Teil der funktionalen Programmierung, die so genannte Memoisierung. Du wirst genau erfahren, wie und wann du den useCallback Hook nutzen kannst, um das Beste aus seinen leistungssteigernden Fähigkeiten zu machen.

Bist du bereit? Dann lass uns loslegen!

Was ist Memoisierung?

Memoisierung bedeutet, dass eine komplexe Funktion ihre Ausgabe speichert, damit sie beim nächsten Aufruf mit derselben Eingabe wieder zur Verfügung steht. Es ist ähnlich wie das Caching, aber auf einer lokaleren Ebene. So können komplexe Berechnungen übersprungen werden und die Ausgabe wird schneller zurückgegeben, da sie bereits berechnet wurde.

Das kann sich erheblich auf die Speicherzuweisung und die Leistung auswirken, und genau diese Belastung soll der useCallback Hook lindern.

Reacts useCallback vs. useMemo

An dieser Stelle ist es erwähnenswert, dass useCallback gut mit einem anderen Hook namens useMemo zusammenarbeitet. Wir werden beide erörtern, aber in diesem Beitrag konzentrieren wir uns auf useCallback als Hauptthema.

Der wichtigste Unterschied ist, dass useMemo einen memoisierten Wert zurückgibt, während useCallback eine memoisierte Funktion zurückgibt. Das bedeutet, dass useMemo zum Speichern eines berechneten Wertes verwendet wird, während useCallback eine Funktion zurückgibt, die du später aufrufen kannst.

Diese Hooks geben eine zwischengespeicherte Version zurück, es sei denn, eine ihrer Abhängigkeiten (z. B. State oder Props) ändert sich.

Schauen wir uns die beiden Funktionen einmal in Aktion an:

import { useMemo, useCallback } from 'react'
const values = [3, 9, 6, 4, 2, 1]

// This will always return the same value, a sorted array. Once the values array changes then this will recompute.
const memoizedValue = useMemo(() => values.sort(), [values])

// This will give me back a function that can be called later on. It will always return the same result unless the values array is modified.
const memoizedFunction = useCallback(() => values.sort(), [values])

Der obige Codeschnipsel ist ein erfundenes Beispiel, zeigt aber den Unterschied zwischen den beiden Rückrufen:

  1. memoizedValue wird zu dem Array [1, 2, 3, 4, 6, 9]. Solange die Wertevariable bestehen bleibt, bleibt auch memoizedValue bestehen und wird nie wieder neu berechnet.
  2. memoizedFunction wird eine Funktion sein, die das Array [1, 2, 3, 4, 6, 9] zurückgibt.

Das Tolle an diesen beiden Callbacks ist, dass sie zwischengespeichert werden und so lange bleiben, bis sich das Abhängigkeitsarray ändert. Das bedeutet, dass sie bei einem Rendering nicht in den Müll geworfen werden.

Rendering und React

Warum ist die Memoisierung bei React so wichtig?

Es hat damit zu tun, wie React deine Komponenten rendert. React nutzt ein virtuelles DOM, das im Speicher gespeichert ist, um Daten zu vergleichen und zu entscheiden, was aktualisiert werden soll.

Das virtuelle DOM unterstützt React bei der Leistung und hält deine Anwendung schnell. Wenn sich ein Wert in deiner Komponente ändert, wird normalerweise die gesamte Komponente neu gerendert. Dass macht  React „reactive“ auf Benutzereingaben und lässt den Bildschirm aktualisieren, ohne die Seite neu zu laden.

Du willst deine Komponente nicht neu rendern, weil sich Änderungen nicht auf die Komponente auswirken werden. Hier erweist sich die Memoisierung durch useCallback und useMemo als nützlich

Wenn React deine Komponente neu rendert, werden auch die Funktionen, die du in deiner Komponente deklariert hast, neu erstellt.

Beachte, dass der Vergleich der Gleichheit einer Funktion mit einer anderen Funktion immer falsch sein wird. Da eine Funktion auch ein Objekt ist, kann sie nur sich selbst gleich sein:

// these variables contain the exact same function but they are not equal
const hello = () => console.log('Hello Matt')
const hello2 = () => console.log('Hello Matt')

hello === hello2 // false
hello === hello // true

Mit anderen Worten: Wenn React deine Komponente neu rendert, werden alle Funktionen, die in deiner Komponente deklariert sind, als neue Funktionen angesehen.

Das ist in den meisten Fällen in Ordnung, denn einfache Funktionen sind leicht zu berechnen und beeinträchtigen die Leistung nicht. Aber wenn du nicht möchtest, dass die Funktion als neue Funktion angesehen wird, kannst du auf useCallback zurückgreifen.

Du fragst dich vielleicht: „Wann sollte ich nicht wollen, dass eine Funktion als neue Funktion erkannt wird?“ Nun, es gibt bestimmte Fälle, in denen useCallback sinnvoller ist:

  1. Du übergibst die Funktion an eine andere Komponente, die ebenfalls memoisiert ist (useMemo)
  2. Deine Funktion hat einen internen Status, den sie sich merken muss
  3. Deine Funktion ist von einem anderen Hook abhängig, wie zum Beispiel useEffect

Leistungsvorteile von React useCallback

Wenn useCallback richtig eingesetzt wird, kann es dazu beitragen, deine Anwendung zu beschleunigen und zu verhindern, dass Komponenten neu gerendert werden, wenn sie es nicht müssen.

Nehmen wir an, du hast eine Komponente, die eine große Menge an Daten abruft und dafür verantwortlich ist, diese Daten in Form eines Diagramms oder einer Grafik darzustellen, etwa so:

Ein buntes Balkendiagramm, das die gesamte Transaktionszeit von PHP, MySQL, Reddis und externen (anderen) Systemen in Millisekunden vergleicht.
Balkendiagramm, das mit einer React-Komponente erstellt wurde.

Angenommen, die übergeordnete Komponente für die Komponente deiner Datenvisualisierung wird neu erstellt, aber die geänderten props oder der state wirken sich nicht auf diese Komponente aus. In diesem Fall willst oder musst du die Komponente wahrscheinlich nicht neu rendern und alle Daten neu abrufen. Wenn du dieses erneute Rendern und Abrufen vermeidest, sparst du die Bandbreite deines Nutzers und sorgst für ein reibungsloseres Nutzererlebnis.

Nachteile von React useCallback

Obwohl dieser Hook die Leistung verbessern kann, hat er auch seine Tücken. Einige Dinge, die du vor der Verwendung von useCallback (und useMemo) beachten solltest, sind:

  • Müllabfuhr: Die anderen Funktionen, die nicht bereits memoisiert sind, werden von React weggeworfen, um Speicher freizugeben.
  • Speicherzuweisung: Ähnlich wie bei der Müllabfuhr gilt: Je mehr memoisierte Funktionen du hast, desto mehr Speicher wird benötigt. Außerdem gibt es jedes Mal, wenn du diese Rückrufe verwendest, einen Haufen Code in React, der noch mehr Speicher benötigt, um dir die zwischengespeicherte Ausgabe zu liefern.
  • Komplexität des Codes: Wenn du anfängst, Funktionen in diese Hooks zu verpacken, erhöht sich sofort die Komplexität deines Codes. Du musst nun besser verstehen, warum diese Hooks verwendet werden und sicherstellen, dass sie richtig eingesetzt werden.

Wenn du dir der oben genannten Fallstricke bewusst bist, kannst du dir die Kopfschmerzen ersparen, wenn du selbst über sie stolperst. Wenn du den Einsatz von useCallback in Betracht ziehst, solltest du sicher sein, dass die Leistungsvorteile die Nachteile überwiegen.

React useCallback Beispiel

Unten siehst du ein einfaches Setup mit einer Button-Komponente und einer Counter-Komponente. Der Zähler hat zwei Zustände und gibt zwei Button-Komponenten aus, die jeweils einen separaten Teil des Zustands der Zähler-Komponente aktualisieren.

Die Button-Komponente nimmt zwei Requisiten auf: handleClick und name. Jedes Mal, wenn der Button gerendert wird, wird er in die Konsole geschrieben.

import { useCallback, useState } from 'react'

const Button = ({handleClick, name}) => {
  console.log(`${name} rendered`)
  return <button onClick={handleClick}>{name}</button>
}

const Counter = () => {

console.log('counter rendered')
  const [countOne, setCountOne] = useState(0)
  const [countTwo, setCountTwo] = useState(0)
  return (
    <>
      {countOne} {countTwo}
      <Button handleClick={() => setCountOne(countOne + 1)} name="button1" />
      <Button handleClick={() => setCountTwo(countTwo + 1)} name="button1" />
    </>
  )
}

Wenn du in diesem Beispiel auf eine der beiden Schaltflächen klickst, siehst du dies in der Konsole:

// counter rendered

// button1 rendered
// button2 rendered

Wenn wir nun useCallback auf unsere handleClick Funktionen anwenden und unseren Button in React.memo umhüllen, können wir sehen, was useCallback uns bietet. React.memo ist ähnlich wie useMemo und ermöglicht es uns, eine Komponente zu memoisieren.

import { useCallback, useState } from 'react'

const Button = React.memo(({handleClick, name}) => {
  console.log(`${name} rendered`)
  return <button onClick={handleClick}>{name}</button>
})

const Counter = () => {
  console.log('counter rendered')
  const [countOne, setCountOne] = useState(0)
  const [countTwo, setCountTwo] = useState(0)
  const memoizedSetCountOne = useCallback(() => setCountOne(countOne + 1), [countOne)
  const memoizedSetCountTwo = useCallback(() => setCountTwo(countTwo + 1), [countTwo])
  return (
    <>
        {countOne} {countTwo}
        <Button handleClick={memoizedSetCountOne} name="button1" />
        <Button handleClick={memoizedSetCountTwo} name="button1" />
    </>
  )
}

Wenn wir jetzt auf eine der Schaltflächen klicken, sehen wir nur die Schaltfläche, die wir angeklickt haben, um uns in der Konsole anzumelden:

// counter rendered

// button1 rendered

// counter rendered

// button2 rendered

Wir haben die Memoisierung auf unsere Schaltflächenkomponente angewendet, und die Prop-Werte, die ihr übergeben werden, werden als gleich angesehen. Die beiden handleClick Funktionen werden zwischengespeichert und von React als dieselbe Funktion angesehen, bis sich der Wert eines Elements im Abhängigkeitsarray ändert (z. B. countOne, countTwo).

Zusammenfassung

So cool useCallback und useMemo auch sind, denk daran, dass sie bestimmte Anwendungsfälle haben – du solltest nicht jede Funktion mit diesen Hooks umhüllen. Wenn die Funktion rechenintensiv ist, ist eine Abhängigkeit von einem anderen Hook oder ein Prop, der an eine memoisierte Komponente übergeben wird, ein guter Indikator dafür, dass du vielleicht useCallback nutzen solltest.

Wir hoffen, dass dieser Artikel dir geholfen hat, diese fortgeschrittene React-Funktionalität zu verstehen, und dass du dabei mehr Vertrauen in die funktionale Programmierung gewinnen konntest!

Matthew Sobieray

Matthew works for Kinsta as a Development Team Lead from his home office in Denver, Colorado. He loves to learn, especially when it comes to web development.