Using requestAnimationFrame with React Hooks

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Das Animieren mit requestAnimationFrame sollte einfach sein, aber wenn Sie die Dokumentation von React nicht gründlich gelesen haben, werden Sie wahrscheinlich auf einige Dinge stoßen, die Ihnen Kopfzerbrechen bereiten könnten. Hier sind drei „Fallen“, die ich auf die harte Tour gelernt habe.

TLDR: Übergeben Sie ein leeres Array als zweiten Parameter für useEffect, um zu verhindern, dass es mehr als einmal ausgeführt wird, und übergeben Sie eine Funktion an die Setter-Funktion Ihres States, um sicherzustellen, dass Sie immer den korrekten State haben. Verwenden Sie außerdem useRef zum Speichern von Dingen wie dem Zeitstempel und der ID der Anfrage.

useRef ist nicht nur für DOM-Referenzen

Es gibt drei Möglichkeiten, Variablen in funktionalen Komponenten zu speichern

  1. Wir können ein einfaches const oder let definieren, dessen Wert bei jeder Neurendern der Komponente immer neu initialisiert wird.
  2. Wir können useState verwenden, dessen Wert über Neurendern hinweg erhalten bleibt und das auch eine Neurenderung auslöst, wenn Sie es ändern.
  3. Wir können useRef verwenden.

Der useRef Hook wird hauptsächlich zum Zugreifen auf das DOM verwendet, aber er ist mehr als das. Es ist ein veränderliches Objekt, das einen Wert über mehrere Neurendern hinweg beibehält. Es ist wirklich ähnlich zum useState Hook, außer dass Sie seinen Wert über seine .current Eigenschaft lesen und schreiben, und das Ändern seines Wertes löst keine Neurenderung der Komponente aus.

Zum Beispiel zeigt das folgende Beispiel immer 5, auch wenn die Komponente von ihrer übergeordneten Komponente neu gerendert wird.

function Component() {
  let variable = 5;

  setTimeout(() => {
    variable = variable + 3;
  }, 100)

  return <div>{variable}</div>
}

Während dieses die Zahl immer um drei erhöht und neu rendert, auch wenn sich die übergeordnete Komponente nicht ändert.

function Component() {
  const [variable, setVariable] = React.useState(5);

  setTimeout(() => {
    setVariable(variable + 3);
  }, 100)

  return <div>{variable}</div>
}

Und schließlich gibt dieses den Wert fünf zurück und rendert nicht neu. Wenn die übergeordnete Komponente jedoch ein Neurendern auslöst, wird der Wert jedes Mal erhöht (vorausgesetzt, das Neurendern erfolgte nach 100 Millisekunden).

function Component() {
  const variable = React.useRef(5);

  setTimeout(() => {
    variable.current = variable.current + 3;
  }, 100)

  return <div>{variable.current}</div>
}

Wenn wir veränderliche Werte haben, die wir uns für das nächste oder spätere Rendern merken möchten und wir nicht möchten, dass sie beim Ändern ein Neurendern auslösen, dann sollten wir useRef verwenden. In unserem Fall benötigen wir die sich ständig ändernde ID von requestAnimationFrame zum Bereinigen, und wenn wir basierend auf der Zeit zwischen den Zyklen animieren, müssen wir den Zeitstempel der vorherigen Animation speichern. Diese beiden Variablen sollten als Refs gespeichert werden.

Die Seiteneffekte von useEffect

Wir können den useEffect Hook verwenden, um unsere Anfragen zu initialisieren und zu bereinigen, obwohl wir sicherstellen wollen, dass er nur einmal ausgeführt wird; andernfalls wird die Animationsrahmen-Anfrage bei jedem Rendern erstellt, abgebrochen und neu erstellt. Hier ist ein funktionierendes, aber schlechtes Beispiel

function App() {
  const [state, setState] = React.useState(0)

  const requestRef = React.useRef()
  
  const animate = time => {
    // Change the state according to the animation
    requestRef.current = requestAnimationFrame(animate);
  }
    
  // DON’T DO THIS
  React.useEffect(() => {
    requestRef.current = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(requestRef.current);
  });
  
  return <div>{state}</div>;
}

Warum ist es schlecht? Wenn Sie dies ausführen, löst useEffect die animate-Funktion aus, die sowohl den State ändert als auch einen neuen Animationsrahmen anfordert. Das klingt gut, außer dass die Zustandsänderung die Komponente neu rendert, indem die gesamte Funktion erneut ausgeführt wird, einschließlich des useEffect Hooks, der zunächst als Bereinigung die im vorherigen Zyklus gemachte Anfrage abbricht und dann eine neue Anfrage startet. Dies ersetzt letztendlich die von der animate-Funktion gemachte Anfrage und ist völlig unnötig. Wir könnten dies vermeiden, indem wir keine neue Anfrage in der animate-Funktion starten, aber das wäre immer noch nicht so schön. Es würde uns immer noch eine unnötige Bereinigung pro Runde hinterlassen, und wenn die Komponente aus irgendeinem anderen Grund neu gerendert wird – z. B. wenn die übergeordnete Komponente sie neu rendert oder ein anderer State sich geändert hat – würden die unnötige Abbrechung und Neuerstellung der Anfrage immer noch stattfinden. Es ist ein besseres Muster, Anfragen nur einmal zu initialisieren, sie von der animate-Funktion am Laufen zu halten und dann einmal zu bereinigen, wenn die Komponente unmounted wird.

Um sicherzustellen, dass der useEffect Hook nur einmal ausgeführt wird, können wir ein leeres Array als zweites Argument übergeben. Das Übergeben eines leeren Arrays hat jedoch einen Seiteneffekt, der uns daran hindert, den korrekten State während der Animation zu haben. Das zweite Argument ist eine Liste von sich ändernden Werten, auf die der Effekt reagieren muss. Wir wollen auf nichts reagieren – wir wollen nur die Animation initialisieren – daher haben wir das leere Array. Aber React wird dies so interpretieren, dass dieser Effekt nicht mit dem State übereinstimmen muss. Und das schließt die animate-Funktion ein, da sie ursprünglich aus dem Effekt aufgerufen wurde. Als Ergebnis, wenn wir versuchen, den Wert des States in der animate-Funktion abzurufen, wird es immer der ursprüngliche Wert sein. Wenn wir den State basierend auf seinem vorherigen Wert und der vergangenen Zeit ändern möchten, wird es wahrscheinlich nicht funktionieren.

function App() {
  const [state, setState] = React.useState(0)

  const requestRef = React.useRef()
  
  const animate = time => {
    // The 'state' will always be the initial value here
    requestRef.current = requestAnimationFrame(animate);
  }
    
  React.useEffect(() => {
    requestRef.current = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(requestRef.current);
  }, []); // Make sure the effect runs only once
  
  return <div>{state}</div>;
}

Die Setter-Funktion des States akzeptiert auch eine Funktion

Es gibt eine Möglichkeit, unseren neuesten State zu verwenden, auch wenn der useEffect Hook unseren State auf seinen Anfangswert gesperrt hat. Die Setter-Funktion des useState Hooks kann auch eine Funktion akzeptieren. Anstatt also einen Wert basierend auf dem aktuellen State zu übergeben, wie Sie es wahrscheinlich meistens tun würden

setState(state + delta)

... können Sie auch eine Funktion übergeben, die den vorherigen Wert als Parameter erhält. Und ja, das gibt auch in unserer Situation den korrekten Wert zurück

setState(prevState => prevState + delta)

Alles zusammenfügen

Hier ist ein einfaches Beispiel, um alles abzurunden. Wir werden alle oben genannten Punkte zusammenführen, um einen Zähler zu erstellen, der bis 100 zählt und dann von vorne beginnt. Technische Variablen, die wir beibehalten und mutieren möchten, ohne die gesamte Komponente neu zu rendern, werden mit useRef gespeichert. Wir haben sichergestellt, dass useEffect nur einmal ausgeführt wird, indem wir ein leeres Array als zweiten Parameter übergeben. Und wir mutieren den State, indem wir eine Funktion an den Setter von useState übergeben, um sicherzustellen, dass wir immer den korrekten State haben.

Siehe den Pen
Using requestAnimationFrame with React hooks
von Hunor Marton Borbely (@HunorMarton)
auf CodePen.

Update: Die Extrameile mit einem benutzerdefinierten Hook gehen

Sobald die Grundlagen klar sind, können wir auch Meta mit Hooks gehen, indem wir die meiste unserer Logik in einen custom Hook extrahieren. Dies hat zwei Vorteile

  1. Es vereinfacht unsere Komponente erheblich und verbirgt technische Variablen, die mit der Animation zusammenhängen, aber nicht mit unserer Hauptlogik
  2. Custom Hooks sind wiederverwendbar. Wenn Sie eine Animation in einer anderen Komponente benötigen, können Sie sie auch dort verwenden

Custom Hooks mögen zunächst wie ein fortgeschrittenes Thema klingen, aber letztendlich verschieben wir nur einen Teil unseres Codes von unserer Komponente in eine Funktion und rufen diese Funktion dann in unserer Komponente auf, genau wie jede andere Funktion. Als Konvention sollte der Name eines benutzerdefinierten Hooks mit dem Schlüsselwort use beginnen, und die Regeln von Hooks gelten, aber ansonsten sind es einfach Funktionen, die wir mit Eingaben anpassen können und die etwas zurückgeben könnten.

In unserem Fall können wir, um einen generischen Hook für requestAnimationFrame zu erstellen, einen Callback übergeben, den unser custom Hook bei jedem Animationszyklus aufruft. Auf diese Weise bleibt unsere Hauptanimationslogik in unserer Komponente, aber die Komponente selbst ist fokussierter.

Siehe den Pen
Using requestAnimationFrame with custom React Hook
von Hunor Marton Borbely (@HunorMarton)
auf CodePen.