Sie wissen, wie einige Websites und Webanwendungen beim Übergang zwischen zwei Seiten oder Ansichten ein schönes natives Gefühl vermitteln? Sarah Drasner hat einige gute Beispiele gezeigt und sogar eine Vue-Bibliothek dazu.
Diese Animationen sind die Art von Funktionen, die eine gute Benutzererfahrung in eine großartige verwandeln können. Um dies in einem React-Stack zu erreichen, ist es notwendig, entscheidende Teile Ihrer Anwendung miteinander zu koppeln: die Routing-Logik und das Animationstooling.
Beginnen wir mit Animationen. Wir werden mit React arbeiten, und es gibt großartige Optionen, die wir nutzen können. Insbesondere ist die react-transition-group das offizielle Paket, das das Ein- und Ausblenden von Elementen aus dem DOM handhabt. Lassen Sie uns einige relativ einfache Muster untersuchen, die wir auch auf bestehende Komponenten anwenden können.
Übergänge mit react-transition-group
Zuerst machen wir uns mit der react-transition-group Bibliothek vertraut, um zu untersuchen, wie wir sie für Elemente verwenden können, die in den DOM ein- und austreten.
Übergänge einzelner Komponenten
Als einfaches Beispiel für einen Anwendungsfall können wir versuchen, ein Modal oder einen Dialog zu animieren – wissen Sie, die Art von Element, das von Animationen profitiert, die es reibungslos ein- und austreten lassen.
Eine Dialogkomponente könnte etwa so aussehen
import React from "react";
class Dialog extends React.Component {
render() {
const { isOpen, onClose, message } = this.props;
return (
isOpen && (
<div className="dialog--overlay" onClick={onClose}>
<div className="dialog">{message}</div>
</div>
)
);
}
}
Beachten Sie, dass wir die `isOpen`-Prop verwenden, um zu bestimmen, ob die Komponente gerendert wird oder nicht. Dank der Einfachheit der kürzlich geänderten API des `react-transition-group`-Moduls können wir dieser Komponente ohne großen Aufwand eine CSS-basierte Übergangsfunktion hinzufügen.
Das Erste, was wir brauchen, ist, die gesamte Komponente in eine weitere `TransitionGroup`-Komponente zu wickeln. Im Inneren behalten wir die Prop, um den Dialog zu mounten oder unmounten, den wir in eine `CSSTransition` wickeln.
import React from "react";
import { TransitionGroup, CSSTransition } from "react-transition-group";
class Dialog extends React.Component {
render() {
const { isOpen, onClose, message } = this.props;
return (
<TransitionGroup component={null}>
{isOpen && (
<CSSTransition classNames="dialog" timeout={300}>
<div className="dialog--overlay" onClick={onClose}>
<div className="dialog">{message}</div>
</div>
</CSSTransition>
)}
</TransitionGroup>
);
}
}
Jedes Mal, wenn `isOpen` geändert wird, ändert sich eine Sequenz von Klassennamen am Root-Element des Dialogs.
Wenn wir die `classNames`-Prop auf `"fade"` setzen, wird `fade-enter` sofort hinzugefügt, bevor das Element gemountet wird, und dann `fade-enter-active`, wenn die Übergangsfunktion startet. Wir sollten `fade-enter-done` sehen, wenn die Übergangsfunktion abgeschlossen ist, basierend auf dem gesetzten `timeout`. Genau dasselbe geschieht mit der `exit`-Klassennamen-Gruppe, wenn das Element kurz vor dem Unmounten steht.
Auf diese Weise können wir einfach eine Reihe von CSS-Regeln definieren, um unsere Übergänge zu deklarieren.
.dialog-enter {
opacity: 0.01;
transform: scale(1.1);
}
.dialog-enter-active {
opacity: 1;
transform: scale(1);
transition: all 300ms;
}
.dialog-exit {
opacity: 1;
transform: scale(1);
}
.dialog-exit-active {
opacity: 0.01;
transform: scale(1.1);
transition: all 300ms;
}
JavaScript-Übergänge
Wenn wir komplexere Animationen mit einer JavaScript-Bibliothek orchestrieren möchten, können wir stattdessen die `Transition`-Komponente verwenden.
Diese Komponente tut nichts für uns, wie es `CSSTransition` tat, aber sie bietet Hooks für jeden Übergangszyklus. Wir können Methoden an jeden Hook übergeben, um Berechnungen und Animationen durchzuführen.
<TransitionGroup component={null}>
{isOpen && (
<Transition
onEnter={node => animateOnEnter(node)}
onExit={node => animateOnExit(node)}
timeout={300}
>
<div className="dialog--overlay" onClick={onClose}>
<div className="dialog">{message}</div>
</div>
</Transition>
)}
</TransitionGroup>
Jeder Hook übergibt den Knoten als erstes Argument an die Callback-Funktion – dies gibt uns die Kontrolle über jede Mutation, die wir wünschen, wenn das Element gemountet oder unmountet wird.
Routing
Das React-Ökosystem bietet viele Router-Optionen. Ich werde react-router-dom verwenden, da es die beliebteste Wahl ist und die meisten React-Entwickler mit der Syntax vertraut sind.
Beginnen wir mit einer grundlegenden Routendefinition
import React, { Component } from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import Home from '../views/Home'
import Author from '../views/Author'
import About from '../views/About'
import Nav from '../components/Nav'
class App extends Component {
render() {
return (
<BrowserRouter>
<div className="app">
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/author" component={Author} />
<Route path="/about" component={About} />
</Switch>
</div>
</BrowserRouter>
)
}
}
Wir möchten drei Routen in dieser Anwendung: Home, Autor und Über uns.
Die `BrowserRouter`-Komponente kümmert sich um die Aktualisierungen des Browser-Verlaufs, während `Switch` entscheidet, welches `Route`-Element basierend auf der `path`-Prop gerendert wird. Hier ist das, *ohne* jegliche Übergänge
Öl und Wasser
Obwohl `react-transition-group` und `react-router-dom` großartige und praktische Pakete für ihre jeweiligen Zwecke sind, kann die Mischung aus ihnen ihre Funktionalität brechen.
Zum Beispiel erwartet die `Switch`-Komponente in `react-router-dom` direkte `Route`-Kinder, und die `TransitionGroup`-Komponenten in `react-transition-group` erwarten `CSSTransition`- oder `Transition`-Komponenten als direkte Kinder von sich selbst. Wir können sie also nicht so verpacken, wie wir es zuvor getan haben.
Wir können Ansichten auch nicht mit demselben booleschen Ansatz wie zuvor umschalten, da dies intern von der `react-router-dom`-Logik gehandhabt wird.
React Keys zur Rettung
Obwohl die Lösung vielleicht nicht so *sauber* ist wie unsere vorherigen Beispiele, ist es möglich, die Bibliotheken zusammen zu verwenden. Das Erste, was wir tun müssen, ist, unsere Routendeklaration in eine Render-Prop zu verschieben.
<BrowserRouter>
<div className="app">
<Route render={(location) => {
return (
<Switch location={location}>
<Route exact path="/" component={Home}/>
<Route path="/author" component={Author} />
<Route path="/about" component={About} />
</Switch>
)}
/>
</BrowserRouter>
Funktional hat sich nichts geändert. Der Unterschied ist, dass wir jetzt kontrollieren, was jedes Mal gerendert wird, wenn sich der Speicherort im Browser ändert.
Außerdem stellt `react-router-dom` jedes Mal, wenn dies geschieht, einen eindeutigen `key` im `location`-Objekt bereit.
Falls Sie damit nicht vertraut sind, identifizieren React Keys Elemente im virtuellen DOM-Baum. Meistens müssen wir sie nicht angeben, da React erkennen wird, welcher Teil des DOM geändert werden muss und ihn dann patchen wird.
<Route render={({ location }) => {
const { pathname, key } = location
return (
<TransitionGroup component={null}>
<Transition
key={key}
appear={true}
onEnter={(node, appears) => play(pathname, node, appears)}
timeout={{enter: 750, exit: 0}}
>
<Switch location={location}>
<Route exact path="/" component={Home}/>
<Route path="/author" component={Author} />
<Route path="/about" component={About} />
</Switch>
</Transition>
</TransitionGroup>
)
}}/>
Das ständige Ändern des Schlüssels eines Elements – selbst wenn seine Kinder oder Props nicht geändert wurden – zwingt React, es aus dem DOM zu entfernen und neu zu montieren. Dies hilft uns, den booleschen Toggle-Ansatz zu emulieren, den wir zuvor hatten, und es ist wichtig für uns, da wir ein einzelnes `Transition`-Element platzieren und es für alle unsere Ansichtsübergänge wiederverwenden können, was uns erlaubt, Routing- und Übergangskomponenten zu mischen.
Innerhalb der Animationsfunktion
Sobald die Übergangs-Hooks bei jeder Standortänderung aufgerufen werden, können wir eine Methode ausführen und jede Animationsbibliothek verwenden, um komplexere Szenen für unsere Übergänge zu erstellen.
export const play = (pathname, node, appears) => {
const delay = appears ? 0 : 0.5
let timeline
if (pathname === '/')
timeline = getHomeTimeline(node, delay)
else
timeline = getDefaultTimeline(node, delay)
timeline.play()
}
Unsere `play`-Funktion erstellt hier eine GreenSock-Timeline basierend auf dem `pathname`, und wir können so viele Übergänge festlegen, wie wir für jede einzelne Route wünschen.
Sobald die Timeline für den aktuellen `pathname` erstellt ist, spielen wir sie ab.
const getHomeTimeline = (node, delay) => {
const timeline = new Timeline({ paused: true });
const texts = node.querySelectorAll('h1 > div');
timeline
.from(node, 0, { display: 'none', autoAlpha: 0, delay })
.staggerFrom(texts, 0.375, { autoAlpha: 0, x: -25, ease: Power1.easeOut }, 0.125);
return timeline
}
Jede Timeline-Methode greift auf die DOM-Knoten der Ansicht zu und animiert sie. Sie können andere Animationsbibliotheken anstelle von GreenSock verwenden, aber das Wichtige ist, dass wir die Timeline im Voraus erstellen, damit unsere Haupt-`play`-Methode entscheiden kann, welche für jede Route ausgeführt werden soll.
Ich habe diesen Ansatz in vielen Projekten verwendet, und obwohl er keine offensichtlichen Leistungsprobleme bei internen Navigationen aufweist, habe ich eine Nebenläufigkeitsfrage zwischen dem anfänglichen DOM-Baumaufbau des Browsers und der ersten Routenanimation bemerkt. Dies verursachte eine visuelle Verzögerung bei der Animation für die erste Ladung der Anwendung.
Um sicherzustellen, dass Animationen in jeder Phase der Anwendung reibungslos ablaufen, gibt es noch eine letzte Sache, die wir tun können.
Profilierung des initialen Ladevorgangs
Hier ist, was ich gefunden habe, als ich die Anwendung nach einem Hard-Refresh in den Chrome DevTools auditiert habe

Sie können zwei Linien sehen: eine blaue und eine rote. Blau repräsentiert das `load`-Ereignis und rot `DOMContentLoaded`. Beide schneiden sich mit der Ausführung der anfänglichen Animationen.
Diese Linien zeigen an, dass Elemente animiert werden, während der Browser noch nicht mit dem vollständigen Aufbau des DOM-Baums fertig ist oder Ressourcen parst. Animationen verursachen erhebliche Leistungseinbußen. Wenn wir möchten, dass etwas anderes passiert, müssen wir warten, bis der Browser mit diesen schweren und wichtigen Aufgaben fertig ist, bevor wir unsere Übergänge ausführen.
Nachdem ich viele verschiedene Ansätze ausprobiert hatte, war die Lösung, die tatsächlich funktionierte, die Animation nach diesen Ereignissen zu verschieben – so einfach ist das. Das Problem ist, dass wir uns nicht auf Ereignis-Listener verlassen können.
window.addEventListener(‘DOMContentLoaded’, () => {
timeline.play()
})
Wenn das Ereignis aus irgendeinem Grund eintritt, bevor wir den Listener deklarieren, wird die von uns übergebene Callback-Funktion nie ausgeführt, und dies könnte dazu führen, dass unsere Animationen nie stattfinden und eine leere Ansicht angezeigt wird.
Da dies ein Nebenläufigkeits- und asynchrones Problem ist, habe ich mich entschieden, Promises zu verwenden, aber dann stellte sich die Frage: Wie können Promises und Ereignis-Listener zusammen verwendet werden?
Indem wir ein Promise erstellen, das aufgelöst wird, wenn das Ereignis eintritt. So geht's.
window.loadPromise = new Promise(resolve => {
window.addEventListener(‘DOMContentLoaded’, resolve)
})
Wir können dies im `head`-Bereich des Dokuments oder direkt vor dem Skript-Tag platzieren, das das Anwendungsbundle lädt. Dies stellt sicher, dass das Ereignis nie eintritt, bevor das Promise erstellt wurde.
Darüber hinaus können wir dank dieser Methode das global exponierte `loadPromise` für jede Animation in unserer Anwendung verwenden. Sagen wir, wir möchten nicht nur die Einstiegsansicht animieren, sondern auch ein Cookie-Banner oder den Header der Anwendung. Wir können jede dieser Animationen einfach *nachdem* das Promise aufgelöst wurde, mit `then` zusammen mit unseren Übergängen aufrufen.
window.loadPromise.then(() => timeline.play())
Dieser Ansatz ist über die gesamte Codebasis wiederverwendbar und eliminiert das Problem, das entstehen würde, wenn ein Ereignis aufgelöst wird, bevor die Animationen ausgeführt werden. Es wird sie aufschieben, bis das `DOMContentLoaded`-Ereignis des Browsers vorüber ist.
Sehen Sie jetzt, dass die Animation erst startet, wenn die rote Linie erscheint.

Der Unterschied zeigt sich nicht nur im Profilierungsbericht – es löst tatsächlich ein Problem, das wir in einem echten Projekt hatten.
Zusammenfassung
Um mich daran zu erinnern, habe ich eine Liste von Tipps für mich erstellt, die für Sie nützlich sein könnten, wenn Sie sich mit Ansichtsübergängen in einem Projekt befassen.
- Wenn eine Animation stattfindet, sollte nichts anderes stattfinden. Führen Sie Animationen erst aus, nachdem alle Ressourcen, Abrufe und Geschäftslogik abgeschlossen sind.
- Keine Animation ist besser als schlechte Animationen. Wenn Sie keine gute Animation erreichen können, ist das Entfernen eine faire Einbuße. Der Inhalt ist wichtiger, und seine Anzeige hat Priorität, bis eine gute Animationslösung vorhanden ist.
- Testen Sie auf langsameren und älteren Geräten. Diese erleichtern es Ihnen, Stellen mit schwacher Leistung zu erkennen.
- Profilieren und stützen Sie Ihre Verbesserungen auf Metriken. Anstatt zu raten, wie ich es getan habe, sehen Sie, ob Sie erkennen können, wo Frames fallen gelassen werden oder ob etwas nicht stimmt, und greifen Sie dieses Problem zuerst an.
Das war's! Viel Erfolg bei der Animation von Ansichtsübergängen. Bitte hinterlassen Sie einen Kommentar, wenn dies Fragen aufgeworfen hat oder wenn Sie Übergänge in Ihrer App verwendet haben, die Sie teilen möchten!
Hallo, ich habe dieses Tutorial befolgt und es ist sehr hilfreich, leider bin ich auf ein Problem gestoßen und habe Schwierigkeiten, es zu überwinden. Meine Anwendung muss derzeit einige Legacy-URLs unterstützen, die über Weiterleitungen gehandhabt werden. Und jedes Mal, wenn eine Weiterleitung stattfindet, erhalte ich eine Warnung: „Warning: You tried to redirect to the same route you’re currently on:“. Hier ist ein Beispielcode.
Dies befindet sich in einer übergeordneten Komponente
Hmm, ich kann sehen, wie das den Übergangsfluss tatsächlich brechen könnte. Das ist eine knifflige Angelegenheit und könnte damit zusammenhängen, wie React Router Weiterleitungen innerhalb einerAnweisung behandelt. Ich schlage vor, dieses Problem im React Router-Repository auf GitHub zu melden. Ich bin sicher, dass sie diesen Fall abdecken oder zumindest eine Umgehungslösung anbieten sollten.
Gibt es eine Möglichkeit, Komponenten zwischen verschiedenen Ansichten zu animieren, so wie Sarah Drasner es mit Vue demonstriert hat? Müsste ich die Komponente zu beiden Ansichten hinzufügen und jeweils einen Start- und Endpunkt definieren, damit es so aussieht, als wäre es dieselbe Komponente?
Habe es herausgefunden :)
Hier ist ein Beispiel
https://codesandbox.io/s/93z163xz44
Feedback willkommen