Strona główna / Artykuły

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.

Wydawca: REACTAPP.TOP13 min czytania2900 słów

Ź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.
  • Gdy dowolna dopasowana wartość wynosi "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 direction będzie opisywał inny aktualizację niż ta, która jest animowana.
  • Gdy 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ę.
  • Obiekt bez klucza 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ć.
  • Wsparcie przeglądarek różni się w zależności od wybranej ścieżki. Ponieważ React sam ustala klasę w zrzutku, podejście oparte na właściwościach obiektu działa we wszystkich przeglądarkach obsługujących View Transition Classes: Chrome 125, Safari 18.2 i Firefox 144. Stylizacja za pomocą :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.
  • Nazwy typów to globalne łańcuchy znaków, które trafiają do przeglądarki bez zmian. Dodaj przedrostek, na przykład 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; addTransitionType jest 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 addTransitionType oraz sekcję dotyczącą personalizacji opartej na typach w dokumentacji ViewTransition.

    Literatura pokrewna