Drive React 19.3 View Transition – анімазіі входу і выходу з JavaScript
Drive React 19 дазваляе карбаваць пераходы між відобразамі з JavaScript за дапамою функцыяў onEnter, onExit, onUpdate і onShare, а таксама за дапамою Web Animations API, чаго не можа зрабіць CSS.
Крыніцы: React 19.3 View Transition Events: Drive Enter, Exit and Share Animations from JavaScript
Чатыры з’явы і тое, што яны отримваюць
Класы View Transition Classes караюцца большайшай часткай рухоў: адзін клас на кожны трыггер плюс CSS keyframes. Яны не можаць выконвацца, калі рух залежыць толькі ад значэнняў, ведомых толькі JavaScript — індекса ў лісте, праганака корыстувальніка, сталай пружыны. Для такых случаў <ViewTransition> адкрывае чатыры атрыбуты з’явы, якія даюць вам псевдо-элементы кадру браузера, ўжо таму вы можете кераваць імі за дапамогою Web Animations API. Шэры матэрыял будуе на трыггерах і типах пераходаў, якія былі рассмотраны раней у серыі React 19.3 View Transition.
<ViewTransition
onEnter={(instance, types) => { /* … */ return () => { /* cleanup */ }; }}
onExit={(instance, types) => { /* … */ }}
onUpdate={(instance, types) => { /* … */ }}
onShare={(instance, types) => { /* … */ }}
>
React выклікае адну з’яву на кожную межу для кожнага пераходу, якая падпадае пад выбраны трыггер; onShare мае прывілей над onEnter і onExit. Кожны калебак получае:
instance.old— кадр “до” у вигляде::view-transition-oldз.animate()
instance.new — „пасля“-знімак ::view-transition-newinstance.group — кантэнер ::view-transition-group, які перамешчаецца і зменшвае/збяльшвае размерыinstance.imagePair — ::view-transition-image-pair, які зберагае оба знімакіinstance.name — значэнне view-transition-name, прызначанае бібліятэкай React (або ваша замена)types — тыпы пераходаў з addTransitionType, або порожняй масавыФункцыя-вызов запускаецца пасля таго, як выпалоўваеся обявленне ready пераходу — знімакі вялікі, і ўжо застосаваны стандартныя анімазіі браузера паўтароўваючыся з усімі CSS-класамі. Павинна быць вярнута функцыя для чысткі. Калі пачынаецца іншы пераход або дакумент ховаецца, браузер заварачвае анімазію; чыстка — гэта спосаб заняць анімазію WAAPI, якая не была завершана.
Адміністрацыя знімка через WAAPI
Выклік .animate(keyframes, options) на instance.new працюе як метод элемента і вяртае об’ект Animation. Шаблон: запускаецца часовая шкала, об’ект захоўваецца, а пазней выкалываецца пад час очышчэння.
onEnter={(instance) => {
const anim = instance.new.animate(
[{ transform: 'translateY(24px) scale(.96)', opacity: 0 }, { transform: 'none', opacity: 1 }],
{ duration: 380, easing: 'cubic-bezier(.2,.9,.25,1.15)', fill: 'both' }
);
return () => anim.cancel();
}}
CSS і JavaScript можу дзейсніць спольную роботу. Перехід межы захоўваецца ў CSS (enter="auto", exit="auto"), а рух — з перэборам, налаштаваным за дапамогою слайдера — выконваецца праз JavaScript.
import { ViewTransition, useState, startTransition } from 'react';
export default function App() {
const [show, setShow] = useState(false);
const [bounce, setBounce] = useState(1.15); return (
<>
<div className="bar">
<button onClick={() => startTransition(() => setShow(s => !s))}>
{show ? 'Dismiss' : 'Show'} notification
</button>
<label>overshoot <input type="range" min="1" max="1.6" step="0.05" value={bounce}
onChange={e => setBounce(Number(e.target.value))} /> {bounce}</label>
</div> {show && (
<ViewTransition
enter="auto"
exit="auto"
default="none"
onEnter={(instance) => {
const anim = instance.new.animate(
[{ transform: 'translateY(28px) scale(.94)' }, { transform: 'none' }],
{ duration: 420, easing: `cubic-bezier(.2,.9,.25,${bounce})`, fill: 'both' }
);
return () => anim.cancel();
}}
onExit={(instance) => {
const anim = instance.old.animate(
[{ transform: 'none' }, { transform: 'translateY(28px) scale(.94)' }],
{ duration: 220, easing: 'ease-in', fill: 'both' }
);
return () => anim.cancel();
}}
>
<div className="toast">🔔 Your build finished in 42 s</div>
</ViewTransition>
)}
</>
);
}
.bar { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; }
.toast { margin-top: 12px; padding: 12px 16px; border-radius: 10px; background: light-dark(#fef3c7, #4a3410); }
/* the fade is CSS; slow the UA default so both layers are visible */
::view-transition-old(*), ::view-transition-new(*) { animation-duration: 420ms; }
Значэнне слайдера знаходзіцца ў закрыцці компонента, таму эвэнт можа яго прыняць у час анімацыі без викорыстання CSS-змэнных чы расчысляваных імен класаў.
Тое, чаго CSS не можа зробіць: паследовы рух за дапамогою закрыцця
Для кожнага элемента патрэбны індэксы, якія задаюцца пасляўзаў. У CSS гэта зазвычай значыць власную характэрыстыку --i і затрымкі, створаныя за дапамою calc(), пры чым гэтыя значэнні ніколі не доходзяць да псевдо-элементаў view-transition. Унутры адбуванняяся змагання індэкс уже ў межах дасяжнасці:
import { ViewTransition, useState, useDeferredValue } from 'react';
const RESULTS = ['Frontend Master — Mock interviews', 'React 19.3, explained', 'View Transitions, part 5', 'Fragment Refs', 'use(browser())'];function Result({ text, index }) {
return (
<ViewTransition
default="none"
enter="auto"
onEnter={(instance) => {
const anim = instance.new.animate(
[{ transform: 'translateX(-16px)', opacity: 0 }, { transform: 'none', opacity: 1 }],
{ duration: 300, delay: index * 70, easing: 'ease-out', fill: 'both' }
);
return () => anim.cancel();
}}
>
<li>{text}</li>
</ViewTransition>
);
}export default function App() {
const [q, setQ] = useState('');
// the input stays urgent; the list follows a deferred value, which is a Transition
const deferredQ = useDeferredValue(q);
const hits = deferredQ ? RESULTS.filter(r => r.toLowerCase().includes(deferredQ.toLowerCase())) : [];
return (
<>
<input placeholder="type 'react' or 'r'" value={q} onChange={e => setQ(e.target.value)} />
<ul className="hits">
{hits.map((h, i) => <Result key={h} text={h} index={i} />)}
</ul>
</>
);
}
input { width: 100%; box-sizing: border-box; }
.hits { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 6px; }
.hits li { padding: 8px 12px; border-radius: 8px; background: light-dark(#f1f5f9, #262a33); }
/* keep the UA fade out of the way; the JS animation owns opacity too */
::view-transition-new(*) { animation: none; }
Важлівы два моменты. Значэнне ::view-transition-new(*) { animation: none } усувае эфект затуманення браузера, тады прозрачнасць контролюецца выключна за дапамою JavaScript; інакш эфекты затуманення накладваюцца. Таксама важлівы параметр fill: 'both' для анімацый з затрымкай: без яго кадр залишаецца абсалютна прозрачным пад час затрымкі, а потым робіць перашчот калі пачынаецца анімацыя.
Іспользаванне типаў перахода ўнутры калебэка
Другі аргумент — это спіс, які ствараецца функцыяй addTransitionType. Яго можна вважаць програмаваным альтэрнатыўным варыянтом для карточак CSS, калі неабходна вылічыць трываласць або напрамацу перахода.
onUpdate={(instance, types) => {
const duration = types.includes('fast') ? 120 : 400;
const dir = types.includes('back') ? -1 : 1;
const anim = instance.new.animate(
[{ transform: `translateX(${dir * 40}px)`, opacity: 0 }, { transform: 'none', opacity: 1 }],
{ duration, easing: 'ease-out', fill: 'both' }
);
return () => anim.cancel();
}}
Калі каратацыя ўособліва простая, лепш выбраць формат клас-об’ект, каб анімацыя заставалася ў CSS і была можна адразу пазірэць. Выкарыстоўваеце эвент, калі патрэбна логіка для вычысленняя.
Рутэры: API навігацыі і кнопка «Назад»
Две правілы рутавання ствараюць проблемы для команд, якія інтегруюць переходы межа відображэнням у кліентскія рутэры.
Перш чым запускаць анімацыю, React чакае, пакуль навігацыя не завершыцца. Рутэры, якія зупіняють эвент navigate з API навігацыі пасля рэндарування, должны абразаваць гэтае обявленне ўнутры useLayoutEffect. Абразаванне ў useEffect стварае цыкл: React чакае на навігацыю, навігацыя чакае на эфект, запланаваны толькі пасля пераходу, а установкі для разработки паведамляюць пра зупінку навігацыі.
Історыя, яка ведаецца через класычны popstate, таксама не падтрымляе анімацыйных пераходаў. Такія навігацыі павінны выконвацца сінхронна, каб прасуванне та восстанавленне формы заставаліся правильнымі, а сінхронная дзеянне не можа падтрымляць пераход між відобразамі — таму React ігнаруе анімацыю. Лепш выкарыстоўваць аб’ект слухача Navigation API navigate, які можа обрабоцваць навігацыю назад/вперед асінхронна:
useEffect(() => {
if (!window.navigation) return; // Chrome 102+, Safari 26.2, Firefox 147
const onNavigate = (event) => {
if (!event.canIntercept) return;
event.intercept({
handler() {
return new Promise((resolve) => {
startTransition(() => {
addTransitionType(event.navigationType === 'traverse' ? 'navigation-back' : 'navigation-forward');
setPath(new URL(event.destination.url).pathname);
});
resolveWhenPainted.current = resolve; // resolved from a useLayoutEffect after commit
});
},
});
};
window.navigation.addEventListener('navigate', onNavigate);
return () => window.navigation.removeEventListener('navigate', onNavigate);
}, []);
Рутэры для працы ў продакшыне таксама фільтруюць змяны хэша ў той жа документе, завантажэння та цілі з іншых вывароў; ад бібліятэкаў чакаецца, што яны забезпечыць такую інтеграцыю, а не кожны прыклад практычнага викорыстоўвання будзе яе ствараць з нуля.
Ранейшы ручны падход
До появы гэтых з’явоў команды яшчэ выкарыстоўвалі псевда-элементы за дапамогою WAAPI, пры чым настаўлялася опцыя pseudoElement:
const vt = document.startViewTransition(() => flushSync(update));
await vt.ready;
document.documentElement.animate(
[{ transform: 'translateY(24px)' }, { transform: 'none' }],
{ duration: 380, pseudoElement: '::view-transition-new(toast)' }
);
Такі пады вымагалі ручнага задавання унікальных імен, змушвалі вас вычысляць, який тригер быў актывацый, калі React заставаўся мовчазным, а таксама вымагалі рэгулярнага стежэння за часамы ў зв’язку з метаданай ready. У той час як атрыбуты здачы падзейкаў даўаюць вам автаматычна заданае імя, інформацію пра тое, які тригер быў запускаецься, прыналежныя типы дадзеных і хук, які актывацыйся адразу пасля таго, калі знімкі стану стаюць готавымі.
Лепш застаўляць короткія анімазыі зі зменой візуальнага стану і перамены лейауту ў CSS, а калецкія функцыі здачы падзейкаў выкорыстоўваць толькі для значэнняў, якім дасканальна патрэбныя клозуры — індэксы, пазіцыі курсора, параметры прыродных анімазый чы тоўстароўкі, або трываласці, заданыя прапускамі функцый. Такое раздзелэнне дазволяе зберагчы стайл-шіты, прыямныя для адладчаў DevTools, для звычайных сцэнарыяў, а ў той жа час застаўляць можлівасць викорыстання WAAPI, калі зменныя CSS у жывом DOM ніколі не доходзяць да шару знімкаў стану.
Калі дэмы выкорыстоўваюць слайдер або полье пошуку разам з анімаваным списком, неабходна розместіць статус у тым жа компоненте, які атрыбутуе <ViewTransition>. Тады функцыя-закрыцце для адбывання падзей вычитае свежыя значэнні без апытку глобальнага реестру анімацый. Якщо калькі меж павінны адбывацца паралельна, неабходна перадаць індэкс як параметр маленькаму дзецячаму компоненту, який сам керуе сваім onEnter, замест таго каб вычысляць затрымкі ў родным компоненте, які не можа бачыць кожную інстанцыю кадру.
Чаму гэта важна
Декларатывныя системы анімацый зрэшты патрабуюць імператыўнага способу выйска з складных ситуацый. Дизайн такога спосабу вялічынае, чы роўні будуць адмовляцца ад бібліятэкі ў першай жа складнай ситуацыі. У гэтым прыкладзе структура простая — чатыры функцыі-вызовы і адна об’екта інстанцыі; гэта раштучна складаеся з CSS, а не заменяе яго, а механізм чысткі запобегае таму, каб перарваныя анімацыі заставалі кадры у заморожаным стане.
Логіка анімацыі таксама знаходзится падэльны элементу, які ёю керуе, у межах стану компонента. Не існуе глобальнага рэўістара імен і няма падзеў пошуку ref пасля запуску.
Аспекты, на якія трэба зважаць
- Завжды вяртайце функцію для чысткі. Якщо забыць
return () => anim.cancel(), то будзе прадставленае знімак адобразжаецца да канца свайго часовага шэрыя пасля перарыву. - Падзеі выконваюцца пасля стандартных дзействаў. Эфекты перехода межу прыстроямі і ключавые кадры класа View Transition Class вже застосованы; анімаванне
opacityу JS складаецца з іх, якщо толькі CSS не задаўanimation: noneдля таго псевдо-элемента. - Адна падзея на кожную межу і кожны пераход. Аднальні элементы запускаюць
onShareтолькі — не ўвайшчы і не выйшчы адразу.
instance.old існуе. Выклік instance.old.animate() у функцыі onEnter нарабатывае на псевдо-элемент, які браузер ніколи не стварыў, і без жадных наследкаў. Анімавайце new пад час падхоўду і old пад час выходу.flushSync абмовляецца ад пераходу цэлкам, таму запісы ніколи не выконваюцца. Тое ж самае делае схованы документ.matchMedia('(prefers-reduced-motion: reduce)') усередине калебэка; CSS-запиты не караюць часовыя шэры WAAPI.popstate ніколи не анімуюцься. Пераканайцеся ў наявнасці функцый window.navigation перад тым, як обяцваць анімаваную історыю.Якщо вы ўсё ж не можаце яго вжываць
Ручны ўзьём пацёгу WAAPI працуюе ў будзь-якай версіі React у браузерах, які падтрымляюць document.startViewTransition. Яго трэба абгорнуць адзін раз:
import { flushSync } from 'react-dom';
export async function transitionWith(update, animateFn) {
if (!document.startViewTransition) { update(); return; }
const vt = document.startViewTransition(() => flushSync(update));
await vt.ready;
const anim = animateFn((pseudo) => ({
animate: (kf, opts) => document.documentElement.animate(kf, { ...opts, pseudoElement: pseudo }),
}));
vt.finished.finally(() => anim?.cancel());
}// usage — you own the names
transitionWith(
() => setShow(true),
(get) => get('::view-transition-new(toast)').animate([{ opacity: 0 }, { opacity: 1 }], { duration: 300 })
);
У такім случае вы тыряце інформацыю пра спрычыненне перашырэння і його типы, і вам неабходна самім задаць view-transition-name. Пад час апгрэйда тэла функцыі animateFn зазвычай пераводзится ў onEnter з незначнымі змянамі.
Джэрелы
- Документацыя React
<ViewTransition>пра запускаванні перашырэнняў, анімацыі на JavaScript і інтеграцыі з рутэрамі - Інструкцыі па викорыстоўванні
addTransitionTypeдля налаштавання анімацый праз запускаванні перашырэнняў - Матерыялы MDN пра
Element.animate()і опцыюpseudoElement - Справака API навігацыі MDN
Команды, які викорыстоўваюць React 19.3 для перехода між відображанням, должны расследзваць API змаганняяк альтэратыўны спосаб, а не як стандарт. Пачніце з класаў і типаў; перайдзіце да onEnter / onExit / onUpdate / onShare толькі тады, калі вычысленні або закрыцце є нэвыхідным. Такая дисципліна дазволяе залишаць большасць перамешчанняў виднымі ў панелі стыляў, адночасна пакрываючы ситуаціі, якія CSS не можа выразіць.