Przejścia widoku z uwzględnieniem kierunku w React z addTransitionType
Dowiedz się, jak za pomocą tagów addTransitionType oznaczyć przejście w React wraz z jego przyczyną, aby ViewTransition, CSS oraz obsługiwanie zdarzeń mogły wybrać kierunek przesuwania bez dodatkowego stanu.
Źródła: addTransitionType in React 19.3: Direction-Aware Animations Without Direction State
Załóżmy proces zakupów składający się z kilku kroków z przyciskami „Dalej” i „Wstecz”. Oba one aktualizują tę samą wartość step, ale animacje powinny się różnić: przesuwanie się do przodu powinno przywoływać nową panelę z prawej strony, a przesuwanie się do tyłu – z lewej. Ponieważ zmiana stanu wygląda identycznie w obu przypadkach, komponent nie ma wbudowanego sposobu, by wiedzieć, jaką animację uruchomić, a klasycznym rozwiązaniem jest oddzielna wartość direction, od której oczekuje się, że nadal będzie prawidłowa w momencie uruchomienia animacji.
React 19.3 wprowadza funkcję addTransitionType, która pozwala w prosty sposób rozwiązać ten problem. Wywołujesz ją wewnątrz startTransition, aby zarejestrować powód aktualizacji, a <ViewTransition> wykorzystuje ten powód do wyboru odpowiedniej animacji. Po przeczytaniu tego tekstu dowiesz się, w których trzech miejscach może być użyty typ przejścia, jak łączą się różne typy, na jakie pułapki należy uważać oraz jak osiągnąć ten sam efekt w starszych wersjach React. Funkcja ta opiera się na samym komponencie <ViewTransition>; jeśli ten komponent jest dla ciebie nowy, zacznij od referencji React dotyczącej tego komponentu.
Cała API w dwóch importach
Funkcja ta znajduje się w głównym pakiecie react, obok funkcji startTransition.
import { startTransition, addTransitionType } from 'react';
Każdy obsługiwacz nawigacji otwiera Transition, oznacza go łańcuchem znaków, a następnie przeprowadza zwykłą aktualizację stanu. Etykietą może być dowolny łańcuch znaków; w tym przypadku dwa obsługiwacze używają next i back.
function goNext() {
startTransition(() => {
addTransitionType('next'); // any string
setStep(s => s + 1);
});
}function goBack() {
startTransition(() => {
addTransitionType('back');
setStep(s => s - 1);
});
}
Kontrakt jest prosty. Funkcja addTransitionType(type) przyjmuje jeden łańcuch znaków i nie zwraca żadnej wartości. Musi być wykonywana wewnątrz funkcji zwrotnej przekazanej do startTransition. Jeśli zostanie wywołana gdzie indziej, wersja rozwojowa Reacta zapisuje ostrzeżenie „addTransitionType może być wywoływana tylko wewnątrz funkcji zwrotnej startTransition()” i odrzuca ten typ. Wielokrotne wywołania są dozwolone, a React gromadzi wszystkie typy w tym samym Transitionie.
Ciekawą częścią nie jest sama funkcja, lecz osoby, które ją używają. Jest ich trzy.
Konsument 1: wartości obiektów w atrybutach ViewTransition
Każda właściwość wyzwalacza w <ViewTransition> (enter, exit, update, share i default) może przyjmować obiekt zamiast prostego nazwy klasy. Obiekt ten mapuje typy przejść na nazwę klasy, wartość "auto" lub "none". Klucz default służy jako rozwiązanie awaryjne, gdy żaden z wymienionych typów nie jest dostępny.
<ViewTransition
enter={{ next: 'from-right', back: 'from-left', default: 'auto' }}
exit={{ next: 'to-left', back: 'to-right', default: 'auto' }}
>
<Step />
</ViewTransition>
Pełny przewodnik sterowany typami
Następujący przewodnik ilustruje to w praktyce. Zwróć uwagę na key={step} w punkcie przejścia: każda zmiana kroku rozmontowuje stary panel i montuje nowy, więc React traktuje to jako wyjście połączone z wejściem, a aktywny typ określa kierunek obu części.
import { ViewTransition, useState, startTransition, addTransitionType } from 'react';
Jedna pomocnicza funkcja go otrzymuje wartość delta kroku oraz nazwę typu, dzięki czemu oba przyciski korzystają z tego samego ścieżki kodu.
const STEPS = ['Cart', 'Address', 'Payment', 'Review'];export default function App() {
const [step, setStep] = useState(0); function go(delta, type) {
startTransition(() => {
addTransitionType(type);
setStep(s => s + delta);
});
} return (
<>
<div className="bar">
<button disabled={step === 0} onClick={() => go(-1, 'back')}>← Back</button>
<span>Step {step + 1} of {STEPS.length}</span>
<button disabled={step === STEPS.length - 1} onClick={() => go(1, 'next')}>Next →</button>
</div> <div className="stage">
<ViewTransition
key={step}
enter={{ next: 'from-right', back: 'from-left' }}
exit={{ next: 'to-left', back: 'to-right' }}
>
<section className="panel">{STEPS[step]}</section>
</ViewTransition>
</div>
</>
);
}
Stylizacje układu umieszczają panel w sposób absolutny wewnątrz ograniczonej sceny, co zapobiega temu, by wyświetlane i nowe zdjęcia ekranu przesuwały się względem siebie.
.bar { display: flex; align-items: center; gap: 12px; }
.stage { position: relative; height: 90px; margin-top: 12px; overflow: hidden; border-radius: 12px; }
.panel {
position: absolute; inset: 0;
display: grid; place-items: center;
font: 600 22px system-ui;
background: light-dark(#e0e7ff, #1e1b4b);
}
Zasady animacji odnoszą się do pseudo-elementów przejść widoku za pomocą klas. Stare zdjęcia ekranu mają kluczowe klatki wyjścia to-left lub to-right, nowe zdjęcia – from-right lub from-left, a wszystkie cztery kluczowe klatki po prostu przesuwają się o pełną szerokość podczas zanikania.
::view-transition-old(.to-left) { animation: 260ms ease-in both to-left; }
::view-transition-old(.to-right) { animation: 260ms ease-in both to-right; }
::view-transition-new(.from-right) { animation: 300ms ease-out both from-right; }
::view-transition-new(.from-left) { animation: 300ms ease-out both from-left; }@keyframes to-left { to { transform: translateX(-100%); opacity: 0; } }
@keyframes to-right { to { transform: translateX(100%); opacity: 0; } }
@keyframes from-right { from { transform: translateX(100%); opacity: 0; } }
@keyframes from-left { from { transform: translateX(-100%); opacity: 0; } }
Zobacz, czego komponent nie zawiera: nigdzie nie ma przechowywanej informacji o kierunku. Kierunek należy do aktualizacji, jest przypisywany w momencie planowania tej aktualizacji i porusza się razem z nią w systemie planowania React. Gdy użytkownik szybko naciśnie przycisk „Wstecz” dwa razy, podczas gdy pierwsza animacja jest nadal w trakcie odtwarzania, React grupuje zadania w kolejce, a druga animacja korzysta z typów zebranych dla tej drugiej transycji. Nie istnieje żaden directionRef, który mógłby doprowadzić do rozbieżności.
Jak React rozwiązuje problem dopasowywania typów
React stosuje następujące zasady, w tej kolejności:
- Gdy pasuje więcej niż jeden typ, nazwy klas są łączone ze sobą i wszystkie one są stosowane.
- Gdy nic się nie dopasowuje, React korzysta z wartości
default; bez niej dana akcja nie powoduje animacji.
"none", ma pierwszeństwo i animacja na tym etapie jest całkowicie pomijana.Konsument 2: rodzaje przejść widoku w CSS
React przekazuje również każdy typ przejścia do przeglądarki jako rodzaj natywnego przejścia widoku. Dzięki temu można ograniczyć zastosowanie animacji w pliku stylów za pomocą pseudo-klasy :active-view-transition-type(), nie ingerując w atrybuty komponentu.
Pierwszym krokiem jest przypisanie elementowi klasy view-transition-class. Ten obraz jest następnie dziedziczony i React go nie zastępuje.
/* give the element a class the snapshot inherits — React leaves it alone */
.panel { view-transition-class: panel; }
Następnie należy napisać reguły, które będą obowiązywać tylko wtedy, gdy trwa przejście określonego typu. Składnia nawiasowa pozwala zachować oba pseudo-elementy dla jednego kierunku razem.
/* applies only while a transition tagged 'next' is running */
:root:active-view-transition-type(next) {
&::view-transition-old(.panel) { animation: 220ms ease-in both to-left; }
&::view-transition-new(.panel) { animation: 280ms ease-out both from-right; }
}
:root:active-view-transition-type(back) {
&::view-transition-old(.panel) { animation: 220ms ease-in both to-right; }
&::view-transition-new(.panel) { animation: 280ms ease-out both from-left; }
}
Ten sam asystent, stylizowany wyłącznie za pomocą CSS
Ta wersja asystenta w ogóle nie przekazuje żadnych właściwości enter ani exit. Granica jest pusta, a plik stylów podejmuje wszystkie decyzje na podstawie aktywnego typu.
import { ViewTransition, useState, startTransition, addTransitionType } from 'react';
Kod komponentu jest poza tym identyczny z pierwszym demo.
const STEPS = ['Cart', 'Address', 'Payment', 'Review'];export default function App() {
const [step, setStep] = useState(0); function go(delta, type) {
startTransition(() => {
addTransitionType(type);
setStep(s => s + delta);
});
} return (
<>
<div className="bar">
<button disabled={step === 0} onClick={() => go(-1, 'back')}>← Back</button>
<span>Step {step + 1} of {STEPS.length}</span>
<button disabled={step === STEPS.length - 1} onClick={() => go(1, 'next')}>Next →</button>
</div>
<div className="stage">
<ViewTransition key={step}>
<section className="panel">{STEPS[step]}</section>
</ViewTransition>
</div>
</>
);
}
Stylizacje panelu teraz zawierają view-transition-class: panel, do czego odwołują się poniższe selektory.
.bar { display: flex; align-items: center; gap: 12px; }
.stage { position: relative; height: 90px; margin-top: 12px; overflow: hidden; border-radius: 12px; }
.panel {
position: absolute; inset: 0;
display: grid; place-items: center;
font: 600 22px system-ui;
background: light-dark(#fce7f3, #4a1d3a);
view-transition-class: panel;
}
Reguły skonfigurowane według typu znajdują się w :root, a kluczowe klatki pozostają niezmienione w porównaniu z wcześniej.
/* The type React forwarded to the browser scopes the whole rule */
:root:active-view-transition-type(next) {
&::view-transition-old(.panel) { animation: 260ms ease-in both to-left; }
&::view-transition-new(.panel) { animation: 300ms ease-out both from-right; }
}
:root:active-view-transition-type(back) {
&::view-transition-old(.panel) { animation: 260ms ease-in both to-right; }
&::view-transition-new(.panel) { animation: 300ms ease-out both from-left; }
}@keyframes to-left { to { transform: translateX(-100%); opacity: 0; } }
@keyframes to-right { to { transform: translateX(100%); opacity: 0; } }
@keyframes from-right { from { transform: translateX(100%); opacity: 0; } }
@keyframes from-left { from { transform: translateX(-100%); opacity: 0; } }
Wybierz ten styl, gdy cała strona powinna reagować na jeden wyzwalacz, na przykład router, który oznacza nawigacje jako navigation-forward lub navigation-back. Wolniej używaj właściwości obiektu, gdy konkretna granica wymaga własnego mapowania. Zachowaj linię view-transition-class: jeśli określisz (*) zamiast (.panel), reguła będzie pasować również do głównego zrzutu, a cała strona przesunie się wraz z panelem.
Konsument 3: funkcje zwrotne zdarzeń
Funkcje zwrotne onEnter, onExit, onUpdate i onShare otrzymują zebrane typy jako swój drugi parametr. Dzięki temu kod animacji imperatywnej może również podjąć odpowiednie decyzje w zależności od przyczyny. W tym fragmencie element, który wchodzi, znajduje się początkowo poza ekranem po lewej stronie przy użyciu opcji back, a w przeciwnym razie po prawej; jest animowany za pomocą Web Animations API, a następnie zwraca funkcję do czyszczenia, która anuluje animację, jeśli React ją przerwie.
<ViewTransition
onEnter={(instance, types) => {
const from = types.includes('back') ? '-100%' : '100%';
const anim = instance.new.animate(
[{ transform: `translateX(${from})` }, { transform: 'none' }],
{ duration: 300, easing: 'ease-out' }
);
return () => anim.cancel();
}}
>
Zwracanie funkcji do czyszczenia jest ważne: bez niej szybko przerwana animacja może pozostawić trwającą animację przypisaną do zrzutu stanu, który już nie istnieje.
Umieszczanie kilku typów w jednej animacji przejścia
Ponieważ React przechowuje każdy dodany typ wewnątrz Transition, można nałożyć osobną kwestię na kierunek animacji. W tym karuzeli pole wyboru „quick” dodaje drugi typ o nazwie quick. Jego klasa jedynie skraca czas trwania animacji, a obie klasy znajdują się razem w snapshotcie.
import { ViewTransition, useState, startTransition, addTransitionType } from 'react';
Pomocnik go zawsze dodaje kierunek i warunkowo dodaje quick. Każdy obiekt prop mapuje quick na klasę o tej samej nazwie, dzięki czemu dopasowana klasa kierunku łączy się z klasą quick.
const SLIDES = ['🍎', '🍋', '🥝', '🍇'];export default function App() {
const [i, setI] = useState(0);
const [quick, setQuick] = useState(false); function go(delta, type) {
startTransition(() => {
addTransitionType(type);
if (quick) addTransitionType('quick');
setI(n => (n + delta + SLIDES.length) % SLIDES.length);
});
} return (
<>
<div className="bar">
<button onClick={() => go(-1, 'back')}>←</button>
<button onClick={() => go(1, 'next')}>→</button>
<label><input type="checkbox" checked={quick} onChange={e => setQuick(e.target.checked)} /> quick</label>
</div>
<div className="stage">
<ViewTransition
key={i}
enter={{ next: 'from-right', back: 'from-left', quick: 'quick' }}
exit={{ next: 'to-left', back: 'to-right', quick: 'quick' }}
>
<div className="slide">{SLIDES[i]}</div>
</ViewTransition>
</div>
</>
);
}
Rozkład jest taki sam jak wcześniej, z wyższą platformą przeznaczoną dla dużych emotikon.
.bar { display: flex; align-items: center; gap: 12px; }
.stage { position: relative; height: 110px; margin-top: 12px; overflow: hidden; border-radius: 12px; }
.slide { position: absolute; inset: 0; display: grid; place-items: center; font-size: 56px; background: light-dark(#fef9c3, #3f3a12); }
Reguły kierunkowe ustawiają celowo wolną animację trwającą 700 ms, a reguła quick modyfikuje tylko wartość animation-duration, pozostawiając kluczowe klatki i funkcję wygładzania bez zmian.
::view-transition-old(.to-left) { animation: 700ms ease-in both to-left; }
::view-transition-old(.to-right) { animation: 700ms ease-in both to-right; }
::view-transition-new(.from-right) { animation: 700ms ease-out both from-right; }
::view-transition-new(.from-left) { animation: 700ms ease-out both from-left; }/* second class on the same snapshot: only the duration changes */
::view-transition-old(.quick), ::view-transition-new(.quick) { animation-duration: 120ms; }@keyframes to-left { to { transform: translateX(-100%); opacity: 0; } }
@keyframes to-right { to { transform: translateX(100%); opacity: 0; } }
@keyframes from-right { from { transform: translateX(100%); opacity: 0; } }
@keyframes from-left { from { transform: translateX(-100%); opacity: 0; } }
To połączenie stanowi prawdziwą korzyść z systemu typów: różne aspekty takie jak kierunek, szybkość czy nacisk mogą być reprezentowane jako oddzielne etykiety zamiast rosnącej macierzy połączonych nazw klas.
Podejście oparte na stanie, które zastępuje
Przed tym API kierunek zazwyczaj znajdował się w stanie komponentu, co jest niewłaściwym miejscem dla niego. Typowa konfiguracja dodaje drugi element stanu:
const [step, setStep] = useState(0);
const [direction, setDirection] = useState('next');
Każdy obsługiwacz zapisuje kierunek, a następnie krok, a wynik renderowania przekształca kierunek w nazwę klasy:
function goNext() {
setDirection('next');
setStep(s => s + 1);
}// then, in render:
<div className={`panel enter-${direction}`} key={step}>…</div>
To rozwiązanie ma trzy wady:
- Dodatkowy stan powoduje ponowny render informacji, które w rzeczywistości nie należą do interfejsu użytkownika.
- Dwa szybkie kliknięcia w przeciwnych kierunkach mogą sprawić, że
directionbędzie opisywał inny aktualizację niż ta, która jest animowana.
step zmienia się z innych powodów, takich jak łącze głębokie lub przycisk restartu, pozostałe kierunek jest stosowane do aktualizacji, z którą nie ma żadnego związku.Biblioteki animacji mają tendencję do ukrywania tego samego problemu za właściwością. Wartość custom={direction} przekazywana do wariantów w Framer Motion nadal jest wartością przechowywaną, tylko lepiej nazwaną.
Dlaczego przeniesienie przyczyny na aktualizację ma znaczenie
Za pomocą addTransitionType przyczyna stanowi część aktualizacji, a nie stanu. Zmiana jest niewielka, ale konsekwencje są znaczące: przyczyna nie może stać się przestarzała, nie może wpływać na niepowiązane aktualizacje i nie powoduje żadnych kosztów renderowania. Tworzy również wspólny kanał komunikacji dla bibliotek. Router może raz oznaczyć nawigacje w przód i w tył, a każdy element <ViewTransition> na dowolnej stronie może reagować na te oznaczenia. Dokumentacja React wskazuje, że oczekuje się, iż routery oraz inne biblioteki będą używać wspólnych nazw typów.
Zespół React stwierdził również, że typy nie są ograniczone wyłącznie do animacji i że planowane są dodatkowe zastosowania przyczyny przejścia. Podstawowy mechanizm, polegający na dołączaniu metadanych do przejścia i ich odczytywaniu w momencie zapisu, jest na tyle uniwersalny, by to umożliwić.
Potencjalne problemy, które należy znać przed wdrożeniem
- Typ ma znaczenie wyłącznie wewnątrz funkcji
startTransition. Poza nią React odrzuca ten typ z ostrzeżeniem, które pojawia się tylko w środowisku rozwojowym, a ponieważ aktualizacja jest pilna, nic się i tak nie animuje. - Typy są usuwane po każdym commitie. Jeśli w trakcie przejścia najpierw wyświetla się element awaryjny
<Suspense>, to ten commit przenosi twoje typy, natomiast późniejsze pokazanie rzeczywistej treści ich nie przenosi. Aby uzyskać animacje uwzględniające kierunek poza obszarem ładowania, dodaj typ ponownie, gdy treść będzie gotowa, albo zaprojektuj przejście z elementu awaryjnego na treść tak, aby nie zależało ono od kierunku. "none"zawsze przeważa. Jeśli jakikolwiek dopasowany element skończy się wartością"none", ten mechanizm jest wyłączany, nawet jeśli inny dopasowany typ dostarczył klasę.
default animuje się tylko dla typów, które są w nim wymienione, więc nieoznaczone przejścia przechodzące przez tę granicę nie będą się animować. Często jest to zamierzone, ale łatwo o to zapomnieć.:active-view-transition-type() wymaga nieco nowszych wersji przeglądarek – Chrome 125, Safari 18.2 lub Firefox 147. Aby sprawdzić aktualną sytuację, zapoznaj się z danymi o kompatybilności na MDN.wizard-next, gdy dwie funkcje na jednej stronie mogą używać nazwy next.Rozwiązanie awaryjne dla React 19.2 i wcześniejszych wersji
Stare wersje React nie mogą korzystać z addTransitionType, ale API przeglądarki nadal może przechowywać ten typ. Tracisz możliwość koordynacji przejścia przez React, ale nie informacje o kierunku. Sam wywołaj document.startViewTransition, przekaż parametr types i ponownie użyj CSS z drugiego elementu bez żadnych zmian. Potrzebujesz funkcji flushSync, aby aktualizacja stanu została zastosowana synchronicznie w ramach funkcji callback do aktualizacji przeglądarki:
import { flushSync } from 'react-dom';
Pomocnik ten również działa poprawnie w trudnych warunkach: gdy przeglądarka nie posiada funkcji startViewTransition, po prostu aktualizuje stan bez animacji.
function go(delta, type) {
if (!document.startViewTransition) {
setStep(s => s + delta);
return;
}
document.startViewTransition({
update: () => flushSync(() => setStep(s => s + delta)),
types: [type],
});
}
Po aktualizacji na wersję 19.3 treść funkcji go wraca do krótkiej wersji startTransition z pierwszej sekcji, a plik stylów w ogóle się nie zmienia.
Główne wnioski
- Traktuj kierunek animacji jako metadane dotyczące aktualizacji, a nie jako stan komponentu;
addTransitionTypejest narzędziem do tego celu. - Korzystaj z typów poprzez atrybuty obiektów dla mapowania na poszczególne granice, poprzez
:active-view-transition-type()dla reguł obowiązujących na całej stronie lub poprzez funkcje zwrotne zdarzeń dla animacji imperatywnych. - Licz typy niezależnie od siebie, na przykład kierunek w połączeniu z prędkością, zamiast wymyślać połączone nazwy klas.
- Pamiętaj o kluczowych aspektach: elementy typu Suspense tracą swoje typy,
"none"przejmuje kontrolę nad wszystkim, a nazwy typów dzielą się globalnym przestrzenią imion. - Aby zapoznać się ze szczegółami pełnej API, sprawdź dokumentację do
addTransitionTypeoraz sekcję dotyczącą personalizacji opartej na typach w dokumentacji ViewTransition.
Literatura pokrewna
- Animowanie revelacji napięcia za pomocą ViewTransition: fallbacki, obrazy, czcionki — Dowiedz się, jak umieszczenie elementów typu Suspense w ViewTransition animuje zmianę od szkieletu na treść, które właściwości zapobiegają zbędnym animacjom oraz jak React czeka na załadowanie obrazów i czcionek.
- Zamiana bibliotek tooltipów na API Popover i pozycjonowanie za pomocą CSS Anchor — Dlaczego tooltipy wymagały bibliotek typu Popper i Floating UI, która funkcja wbudowana teraz rozwiązuje problemy z układem, pozycjonowaniem i zamknięciem elementów, oraz kiedy biblioteka JavaScript nadal jest przydatna.