Галоўная / Артыкулы

Переходы відображэння, якія лічуць напрамак руху, у React з addTransitionType

Дазвольце дазнаць, як адзінакоўваць тэгамі addTransitionType пераходы у React разам з іх прычыной, каб ViewTransition, CSS і обробнікі змян моглі выбраць напрамак пераходу без дадатковага статаусу.

Выдавец: REACTAPP.TOP13 хв чытання2900 слоў

Крыніцы: addTransitionType in React 19.3: Direction-Aware Animations Without Direction State

Уявіце сабэршаговы процес адзначэння пакуя з кнопкамі «Наступны» і «Зворачыць». Якія-гэтыя кнопкі адкорэкуюць той самы значэнне step, пры чым анімацыі должны быць разныя: праходжэнне вперад павінна адкрываць новы панель з правага, а праходжэнне назад — з левага. Паколькі змяна стану выглядае ідэнтычна ў будзь-якім разе, компонент не мае вбудованага способу з’ясаваць, якая анімацыя трэба практыкаваць, і класычны спосаб прыемлівання — это адзінэчны значэнне direction, якое, як спадзяваецца, застанется правдазным калі анімацыя нарэшце будзе выканана.

У версіі React 19.3 быў дадзены метод addTransitionType, які дапамагае чыста рашыць гэту проблему. Яго вызываюць унутры startTransition, каб зафіксаваць прычыну апдэйта, а <ViewTransition> выкарыстоўвае гэтую прычыну для выбору анімасцы. Працаваўшы з гэтым матэрыялам, вы дазнаецеся пра тры месца, дзе можа быць выкарыстаны тип пераходу, як спалучаюцься разныя типы, якімы апасцераганням следзіць і як досягнуць таго ж эфекта ў старэйшых версіях React. Гэта рашэнне базуецца на самай складовай <ViewTransition>; якщо гэтая складовая нова для вас, спачатку ознайоміцеся з ўказвікам React да яе.

Усё API за два імпорты

Функцыя знаходзіцца ў галоўным пакете react, рэкі ў startTransition.

import { startTransition, addTransitionType } from 'react';

Кожны працоўнік навігацыі ачынае переход, пазначае яго стрэлкамі і пасля гэтага выкааноўвае звычную апдэйтаванне стану. Пазначка можа быть будзь-ямі стрэлкамі, якія вам паспадобяцца; тут два працоўнікі викорыстоўвають next і back.

function goNext() {
  startTransition(() => {
    addTransitionType('next');   // any string
    setStep(s => s + 1);
  });
}function goBack() {
  startTransition(() => {
    addTransitionType('back');
    setStep(s => s - 1);
  });
}

Контракт ў меры маленькі. Функцыя addTransitionType(type) прымае адна стрэлкі і не вяртае жаданага значэння. Яна павінна выконваліцца ўнутры калбэка, які пасылаецца функцыі startTransition. Якщо яе вызваць гдзе-небудзь іншае, версія React для разработкі запісвае паведамленне "addTransitionType можа быць вызвана толькі ўнутры калбэка startTransition()" і адкідае даныя пра тип. Дазволеныя колькасць вызоў, і React збірае всі типы ў адны переход.

Інтэреснае не ў самай функцыі, а у тых, хто яе чытае. Їх троўці.

Корыстнік 1: значэнні об’ектаў у атрыбутах ViewTransition

Кожны элемент-трыгер у <ViewTransition> (enter, exit, update, share і default) можа прыймаць об’ект заместо простага назвы класу. Што дае можлівасць супарабоўваць типы пераходу з назвай класу, значэнням "auto" або "none". Ключ default выступае як альтэрнатыва, калі жаданага з пераказанных типоў няма.

<ViewTransition
  enter={{ next: 'from-right', back: 'from-left', default: 'auto' }}
  exit={{ next: 'to-left', back: 'to-right', default: 'auto' }}
>
  <Step />
</ViewTransition>

Полны візард, керованы типамі

Наступны візард практычна дэманстрацыя гэтага. Зверніце ўвагу на key={step} на межы: кожная зміна крока дэактывае старую панель і актывае новую, таму React спрацьвоўвае гэта як выхід і вход, а актыўны тип вялічы напрамак дзейства обох частак.

import { ViewTransition, useState, startTransition, addTransitionType } from 'react';

Адны памочны функцыя go прымае разлік крокаў і назву типу, таму обе кнопкі выкарыстоўваюць аднойчы той самы кодавы шлях.

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>
    </>
  );
}

Стылі макета размешчаюць панель абсалютна ўнутрь обмежанай сцэны, што запобегае таму, каб выходзячыя і вхідныя знімкі перасувалі одзін другога.

.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);
}

Правіла анімацыі нацеленыя на псевдо-элементы пераходу відображення за допаможчыю класаў. Старыя знімкі маюць ключавыя кадры to-left або to-right для выходу, новыя знімкі — from-right або from-left, і чатыры ключавыя кадры проста перасуваюцься на весь шырокі розмер пад час затуманення.

::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; } }

Паглядзіце, чаго няма ў компаненте: ніде не схована напрамоць. Напрамоць належыць да апдэйта, яна прыўязываецца ў момент планавання апдэйта і пераходзіць разам з яным через скаржыльнік React. Калі корыстнік швыдка нажме кнопку «Назад» два разы, пакуль яшчэ пераглядаецца першы слайд, React агрупавае запланаваныя задачі, і другая анімацыя чытае тыпы, збораныя для гэтага другога переходу. Не існуе directionRef, які мог бы выйсці з синхроназы.

Як React вырашае пытанне адпаведных тыпоў

React прыменяе гэтыя правілы ў такой спраўе:

  • Калі падходзіць больш ад однаго тыпу, імены відпаведных класаў з’яеднваюцца і прыменяюцца ўсе яны.
  • Калі нічога не падходзіць, React пераходзіць на значэнне default; без яго такі тригер не стварае анімацыі.
  • Калі будь-яй падак, які супараднаецца, ў значэнні "none", ён мае прыоритэт, і анімазія на граніцы ўзагалі не выконваецца.
  • Корыстнік 2: типы перахода відношэнняў у CSS

    React таксама перадае кожны тип пераходу браузеру як тип перахода відношэнняў у прыродным формате. У результате вы можетэ адзначаць дыярэжу анімазій у файле стылю за дапамою псевдокласа :active-view-transition-type() і не чыніць змян у параметрах компонента.

    Першы крок — даць элементу клас view-transition-class. Этот клас успадковываецца, і React яго не перазапісвае.

    /* give the element a class the snapshot inherits — React leaves it alone */
    .panel { view-transition-class: panel; }
    

    Потым напішыце правілы, якія праўяцца толькі калі выконваецца пераход у заданым типе. Синтаксіс вярсткавання дазволяе заставіць оба псевдо-элементы разам для аднаго напрамку.

    /* 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; }
    }
    

    Той самы візард, стайлізаваны толькі за дапамою CSS

    Эта версія візарда зовсім не перадае параметры enter чыра exit. Граніцы не маюць нічога, а стайл-шіт берэ ўсе рашэнні з актыўнага типу.

    import { ViewTransition, useState, startTransition, addTransitionType } from 'react';
    

    Код компонента іншым спосабам ідэнтычны першай дэманстрацыі.

    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>
        </>
      );
    }
    

    Стайлы панелі тепер включаюць view-transition-class: panel, які і викорыстоўваюць наведзеныя нижэй селектары.

    .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;
    }
    

    Правілы, прыязначаныя да певнага типу, знаходзяцца на :root, а ключавыя кадры застаюцца такімі ж, як і раней.

    /* 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; } }
    

    Выберыце гэты стыль, калі весь адзін старонкі должна рэагаваць на адну прычыну, напрыклад, рутэр, який пазначае навігацыю як navigation-forward або navigation-back. Калі патрэбна адна конкрэтная граніца з сае мапаваннем, лепш выкарыстоўваць атрыбуты об’екта. Залейчыце лінію view-transition-class: якщо вы назначыце (*) заместо (.panel), правіла таксама будуць прыменяваны да корневага скріншота, і весь старонкі будзе перасувацца разам з панеллю.

    Consumer 3: event callbacks

    onEnter, onExit, onUpdate і onShare функцыі-адаптаванні прыймаюць збораныя типы як свой другі параметр. Гэта таксама дазволяе коду анімазій у формате імператыўных запаведзей робіць розгалужэння ў залежнасці ад прычыны. У гэтым фрагменте элемент, які заходзіць, спачатку знаходзіцца за межама екрана: слева праз кнопку back, а інакш — справа; ён анімуецца за дапамою Web Animations API, і вяртае функцыю для чысткі, якая анулюе анімазію, якщо React яе перарве.

    <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();
      }}
    >
    

    Вярненне функцыі для чысткі мае значэнне: без яе раштоўна перарваны пераход мога залишыць анімазію, якая ўсё ўжо працуе, прычаленай да копіі, якая больш не існуе.

    Размешчэнне калькі типоў у адны пераход

    Пакалькі React зберагае кожны тип, які быў дадзен унутрь Transition, таму можна дадаць незалежную настройку вышэй за напрамоўкай. У гэтым каруселі прапусканне „quick“ дадае другі тип пад назвай quick. Яго клас толькі скрацвае трываласць, і оба класы застаюцца разам у snapshot-е.

    import { ViewTransition, useState, startTransition, addTransitionType } from 'react';
    

    Падмога go завжды дадае напрамоўку і за адпаведных умов дадае quick. Кожны об’ект prop супарабатоўвае quick з класам той жа назвы, такім чынам клас напрамоўкі і клас 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>
        </>
      );
    }
    

    Лейаут адпаведзае той жа ідеі, як і ранейш, з вышэйшай плошчынай для большых эмодзі.

    .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); }
    

    Правілы напрамоўкі задаюць намерна медленную анімацыю трываласцю 700 мс, а правіло quick перакрывае толькі animation-duration, залейваючы ключавыя кадры і параметры плавнасці без змян.

    ::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; } }
    

    Гэта структура ўскладненняў являе сабою практычны выгоды з системы типаў: абліквітныя праблемы, такія як направлення, швальнасць чыта акцэнт, можна выражаць як окалечныя атрыбуты, замест таго каб выкарыстоўваць зростаючую матрыцу сумаваных назв класаў.

    Падход, які ён заменяе

    Да выходу гэтага API направлення зазвычай знаходзілася ў стане компонента, чыя гэта не яўляеся належным месцам. Тыповая наладка дагадвае другі элемент стану:

    const [step, setStep] = useState(0);
    const [direction, setDirection] = useState('next');
    

    Кожны працоўнік запісвае направлення, а пасля — крок, і рэзультат відрасунку перетварае направлення на назву класа:

    function goNext() {
      setDirection('next');
      setStep(s => s + 1);
    }// then, in render:
    <div className={`panel enter-${direction}`} key={step}>…</div>
    

    У гэтам патэрне є тры слабасі:

    • Дадатковы стан спрычыняе паўтарны відрасунак для інфармацыі, якая на самай працэ ня ўжо ёсць часткай UI.
    • Два швальных клікаў у працупаказваных направленнях можу заставіць direction описваць іншую змяну, чым тая, якая анімуецца.
  • Калі step змянюецца з іншых прычын, напрыклад, завяроўка па глęбокім звязку або кнопка перачысця, застаўшыся напрамленне прыкладзваецца да апдэйта, які з гэтым не мае нічынага.
  • Бібліятэкі анімацый часта маскуюць тую ж проблему за праблемай. Значэнне custom={direction} з Framer Motion, якое перадаецца варіантам, таксама застаецца зберажаным значэнням, толькі з лепшай назвой.

    Чаму важна пераказаць прычыну ў апдэйт

    За дапамою addTransitionType прычына ўражання ёсць часткаю апдэйту, а не стану. Змена ў меры незначная, але наследкі значныя: прычына не можа стаць застарэлай, не може паўтарыцца ў некалякуючых апдэйтах і не выклікае додатковых расчыслаў. Ён таксама стварае спяльны канал для бібліятэкаў. Маршрутазёр можа атрыбутаваць прызначэння для переходаў у напрацоўку і назад адно раз, а кожны <ViewTransition> на будзь-яй сторонцы можа реагаваць на гэтыя атрыбуты. У дакументацыі React сказана, што апэкаваецца тым, каб маршрутазёры і іншыя бібліятэкі выкарыстоўвалі спяльныя назвы типаў.

    Команда React таксама заявіла, што типы не абмежваюцца толькі анімацыяй, і плануецца ўжо больш застосоўваń прычыны уражання. Адпаведны механізм, які заключаецца у прыўязванні метаданных да уражання і ў яных чытанні пад час зберагаўання, дастаточна універсальны, каб падтрымаць гэта.

    Проблемы, якія трэба знать пры выпуску

    • Тып урахоўваецца толькі ўнутрь startTransition. За яго межамі React адхоўляе тып і падае апавешчэнне, якое з’яўляецца толькі пад час розрабоцы, і адтуды, што апдэйт ўскораны, нічога не анімуецца.
    • Тыпы чыстяцца пасля кожнага commit. Якщо Transition спачатку адобразыць <Suspense> як альтэрнатыўу, тады той commit зберагае вашы тыпы, а пазнейшая адобразаецца фактычная контэнт не зберагае іх. Для анімазій, якія ведаюць напрамак па межы завантажэння, зноў дадзіце тып, калі контэнт будзе гатовы, або спроектуйце замену альтэрнатывы на контэнт так, каб яна не залежала ад напрамка.
    • "none" завжды мае прывілей. Якщо якая-небудзь падходжаючая ентрыта вырашваецца як "none", тады такі трыгер выключаецца, нават калі іншы падходжаючы тып задаў клас.
  • Об’ект без ключа default анімавае толькі для тых типаў, якія ён перыявляе, таму Transitions без атрыбутаў, якія праходзяць через гэтыя межы, не будуць анімавацца. Гэта часта ўсвядомленая практыка, але лёгка працягнуць.
  • Падтрымка браузерамі разніцяецца ў залежнасці ад выбранага падходу. Паколькі React сам ставя клас на кэшаваны экземпляр, падход з аб’ектамі-атрыбутамі працюе ў будзь-ям браузеры, які падтрымляе View Transition Classes: Chrome 125, Safari 18.2 і Firefox 144. Стылізаванне за дапамой :active-view-transition-type() выконваецца толькі ў Chrome 125, Safari 18.2 або Firefox 147. Для актуальных даных пагляньце на інформацыю MDN.
  • Назвы типаў — глобальныя строкі, якія прыходзяць у браузер без змян. Дадайце ўсё ж такі прыфікс, напрыклад wizard-next, калі два функцыяналы на адной сторонцы можаць выкарыстоўваць next.
  • Альтэрнатыва для React 19.2 і ранейшых версій

    Старэйшыя версіі React не можаць выкарыстоўваць addTransitionType, але API прыглядача все ўсё можа несці інфармацыю пра тип. Вы тады відмовляецеся ад коордынацыі пераходу з боку React, але не від інфармацыі пра направленне. Самі вызывайце document.startViewTransition, перадаючы types, і зноў выкарыстоўвайце CSS з другога корыстніка без жадных змян. Вам патрэбна функцыя flushSync, каб апдэйт стану быў застосаваны сінхронна ўнутры калбэка апдэйта прыглядача:

    import { flushSync } from 'react-dom';
    

    Памагальны код таксама працюе нормальна: калі прыглядач не мае функцыі startViewTransition, ён проста апдэйтуе стан без анімасцы.

    function go(delta, type) {
      if (!document.startViewTransition) {
        setStep(s => s + delta);
        return;
      }
      document.startViewTransition({
        update: () => flushSync(() => setStep(s => s + delta)),
        types: [type],
      });
    }
    

    Пасля апгрэйда да версіі 19.3 тэла функцыі go знову становіцца короткай версіей startTransition з першага раздзела, а стаялка стылю не змінюецца ўжо ўсё.

    Ключовыя выводы

    • Спрыткавайце напрамак анімазіі як метаданы пра актуалізацыю, а не як стан компонента; addTransitionType — гэтыя інструмент для цього.
    • Іспользуйце тыпы через атрыбуты об’ектаў для карэльвання па кожным межавым рубіку, через :active-view-transition-type() для правіл на всю сторанку, або через калебачыні здарэнняў для імператывных анімазій.
    • Складзіце незалежныя тыпы, такія як напрамак плюс швальнасць, замест таго каб ствараць сумаваныя назвы класаў.
    • Памятайце пра важлівыя моменты: элементы Suspense трапляюцца без своіх тыпаў, "none" перакрывае ўсё, а назвы тыпаў дзеляцца глобальным простраменнем імен.
    • Для дакладнай інфармацыі пра API адзірніцеся на дапамогу да addTransitionType і раздзіл пра налаштаванне на аднойчыні з тыпамі ў документах ViewTransition.