Переходи відображення з урахуванням напрямку в React за допомогою addTransitionType
Дізнайтеся, як додати теги addTransitionType до React Transition разом із його причиною, щоб ViewTransition, CSS та обробники подій могли вибрати напрямок переходу без додаткового стану.
Джерела: 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. Його клас лише скорочує тривалість, і обидва класи опиняються разом у знімку.
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>
У цього підходу є три слабкості:
- Додатковий стан спричиняє повторне відображення інформації, яка насправді не є частиною інтерфейсу.
- Два швидкі кліки у протилежних напрямках можуть призвести до того, що змінна
directionбуде відображати іншу зміну, ніж та, яка анімується.
step змінюється з інших причин, наприклад через глибоке посилання чи кнопку перезапуску, залишкове значення напрямку застосовується до оновлення, з яким воно не має нічого спільного.Бібліотеки анімацій зазвичай приховують цю саму проблему за певним параметром. Значення custom={direction}, яке передається до варіантів у Framer Motion, все одно залишається збереженим значенням, просто з кращою назвою.
Чому важливо перемістити причину у саме оновлення
За допомогою addTransitionType причина є частиною оновлення, а не стану. Зміна є незначною, але наслідки значні: причина не може стати застарілою, не може впливати на непов’язані оновлення та не вимагає додаткової обробки під час відображення. Це також створює спільний канал для бібліотек. Маршрутизатор може позначити напрямки переходу вперед та назад один раз, і кожен елемент <ViewTransition> на будь-якій сторінці може реагувати на ці позначки. У документації React зазначено, що очікується, що маршрутизатори та інші бібліотеки будуть використовувати спільні імена типів.
Команда React також зазначила, що типи не обмежуються анімацією, і планується більше сценаріїв використання причини переходу. Основний механізм — додавання метаданих до переходу та їх читання під час збереження змін — достатньо універсальний, щоб підтримати це.
Проблеми, про які потрібно знати перед випуском
- Цей тип враховується лише всередині
startTransition. Поза ним React видаляє цей тип із попередженням, яке з’являється лише під час розробки, і оскільки оновлення є терміновим, ніщо все одно не анімується. - Типи очищуються після кожного коміту. Якщо Transition спочатку відображає замінник
<Suspense>, цей коміт зберігає ваші типи, тоді як подальше відображення справжнього контенту їх не зберігає. Для анімацій, що враховують напрямок через межу завантаження, додайте тип знову, коли контент буде готовий, або спроєктуйте заміну замінника на контент так, щоб вона не залежала від напрямку. "none"завжди має перевагу. Якщо будь-який зі знайдених елементів дорівнює"none", цей тригер вимикається навіть у разі, якщо інший знайдений тип надав клас.
default анімується лише для типів, які він перелічує, тому анімація не відбудеться у безпозначених переходах, які проходять через цю межу. Це часто є навмисним, але легко проігнорувати.: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.