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
- Wir können ein einfaches
constoderletdefinieren, dessen Wert bei jeder Neurendern der Komponente immer neu initialisiert wird. - Wir können
useStateverwenden, dessen Wert über Neurendern hinweg erhalten bleibt und das auch eine Neurenderung auslöst, wenn Sie es ändern. - Wir können
useRefverwenden.
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
- Es vereinfacht unsere Komponente erheblich und verbirgt technische Variablen, die mit der Animation zusammenhängen, aber nicht mit unserer Hauptlogik
- 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.
Danke für die Lektüre. Neu bei React. Gut, direkt einzusteigen. Schön, dass CSS-Tricks sein Ökosystem kennt!
Eine schöne Ergänzung wäre, die RAF-Logik in einen wiederverwendbaren Hook zu extrahieren: https://codepen.io/testerez/pen/QWLGzee?editors=0010
Stimmt, anfangs dachte ich, es sei schon kompliziert genug, aber dann habe ich auch eine Version mit einem benutzerdefinierten Hook hinzugefügt. Ich habe mich für eine Lösung entschieden, die Ihrer sehr ähnlich ist, außer dass Sie die Zeit seit Beginn berechnen und ich eine Kombination aus dem letzten State und der Zeit zwischen zwei Zyklen verwende. In Ihrem Fall stimmt es, dass Sie die Zahl beim Setzen des States runden können, aber bei mir ist das tatsächlich nicht der Fall.
Auch wenn Sie beim Setzen des States runden, werden unnötige Renderings vermieden.
Danke für diesen Beitrag. Das Problem ist, dass selbst wenn man eine einfache Animation machen möchte, sie sehr ruckelig wird. Ich kenne nicht den genauen Mechanismus hinter dem React-Rendering, aber ich nehme an, sie verwenden Promises. Im Browser wird rAF verarbeitet, bevor er überhaupt zu CSS und dem Rendern von Elementen gelangt. Aber das React-Rendering ist nicht dasselbe wie das Browser-Rendering. Daher können wir hier nicht dasselbe Ergebnis erwarten.
Eine einfache SVG-Animation mit diesem Ansatz
https://codesandbox.io/s/requestanimationframereact-dgfk5
Ihr Animationsbeispiel ist ein sehr spezifischer Anwendungsfall für die Erstellung eines Zählers von 1 bis 100. Wenn ich an Animation denke, denke ich an das Bewegen von Dingen. Es ist mir nicht klar, wie ich die Bewegung einer Box von links nach rechts in React mit requestAnimationFrame basierend auf Ihrem Artikel animieren würde.