React 19 ViewTransition: плавные анимации списков задач
-

Фронтенд развивается в сторону более гладких и приятных взаимодействий, и View Transitions API в React 19 - это именно то, что нужно для создания киллер-анимаций переходов. Вместо того чтобы писать кучу CSS-анимаций и синхронизировать их с React-рендером, теперь можно использовать встроенный механизм, который сам разбирается с координацией между DOM и браузером.
В этой статье мы разберёмся, как использовать ViewTransition компонент и startTransition, чтобы список задач переходил в новое состояние не просто мгновенно, а с красивыми анимациями. И без необходимости вешать на элементы кучу состояний и условных классов.
Как это вообще работает
ViewTransition API - это не просто очередной хук или компонент. Это встроенный механизм браузера, который захватывает снимок текущего состояния DOM, затем отрисовывает новое состояние и создаёт гладкую анимацию между ними. React 19 обёрнул это в удобный компонент, который решает главную проблему: синхронизацию рендера с анимацией.
Без этого компонента пришлось бы вручную вызывать
document.startViewTransition()и следить за тем, чтобы состояние обновилось в нужный момент. С ViewTransition всё происходит автоматически - ты просто оборачиваешь нужные элементы и указываешь, какую анимацию применять в каких случаях.Ключевой момент: API применяет
view-transition-nameк DOM-узлам, так что каждый элемент получает уникальный идентификатор для анимации. Браузер создаёт две группы псевдо-элементов -::view-transition-oldи::view-transition-new- и ты можешь анимировать переход между ними через обычные@keyframes.- Нет необходимости в классах состояния - компонент сам управляет тем, какие элементы какие псевдо-элементы получают
- Браузер оптимизирует рендер - это не просто CSS-анимация, а встроенная в браузер оптимизация с использованием paint и composite операций
- Работает со Suspense и deferred обновлениями - можно использовать вместе с другими React-механизмами для асинхронных операций
ViewTransition компонент в деле
Давай сразу посмотрим на практику. Представим, что у тебя есть список задач, и при переходе между состояниями (добавление, удаление, изменение) ты хочешь, чтобы элементы плавно появлялись или исчезали.
import { ViewTransition, useState, startTransition } from 'react'; function TaskItem({ task, onToggle, onDelete }) { return ( <ViewTransition enter={{ 'task-add': 'task-slide-in', 'task-update': 'task-pulse' }} exit={{ 'task-remove': 'task-slide-out' }} > <div className="task-item"> <input type="checkbox" checked={task.completed} onChange={() => onToggle(task.id)} /> <span>{task.title}</span> <button onClick={() => onDelete(task.id)}>✕</button> </div> </ViewTransition> ); } function TaskList() { const [tasks, setTasks] = useState([]); const [newTaskTitle, setNewTaskTitle] = useState(''); const addTask = () => { startTransition(() => { setTasks(prev => [...prev, { id: Date.now(), title: newTaskTitle, completed: false }]); setNewTaskTitle(''); }); }; const toggleTask = (id) => { startTransition(() => { setTasks(prev => prev.map(t => t.id === id ? { ...t, completed: !t.completed } : t)); }); }; const deleteTask = (id) => { startTransition(() => { setTasks(prev => prev.filter(t => t.id !== id)); }); }; return ( <div className="task-list"> <input type="text" value={newTaskTitle} onChange={e => setNewTaskTitle(e.target.value)} placeholder="Новая задача..." /> <button onClick={addTask}>Добавить</button> {tasks.map(task => ( <TaskItem key={task.id} task={task} onToggle={toggleTask} onDelete={deleteTask} /> ))} </div> ); }Видишь, как просто? Оборачиваем элемент в ViewTransition, указываем типы анимаций для входа (
enter) и выхода (exit), а потом всё, что нужно - это обновить состояние внутриstartTransition. Компонент сам разберётся с тем, когда и как применять анимации.Можно использовать addTransitionType, чтобы динамически выбирать тип анимации в зависимости от контекста:
enter- объект с типами анимаций при появлении элементаexit- объект с типами при удаленииonEnter,onExit,onUpdate,onShare- колбэки для полного контроля над процессом анимации
startTransition и управление типами анимаций
startTransition - это обёртка над React-обновлением состояния, которая сигнализирует браузеру: “Сейчас произойдёт изменение DOM, подготовь анимацию”. Это не просто setTimeout с задержкой, это встроенный механизм, который браузер понимает и оптимизирует.
А когда нужна более гибкая логика - когда анимация зависит от направления свайпа, контекста или внешних данных - в помощь addTransitionType. Функция позволяет указать, какой конкретный тип анимации должен быть применён при следующем обновлении.
Примитивный пример: галерея изображений, где свайп влево показывает предыдущее фото с одной анимацией, а свайп вправо - следующее с другой. Вот как это выглядит:
import { startTransition, addTransitionType, useState } from 'react'; function ImageGallery({ images }) { const [currentIndex, setCurrentIndex] = useState(0); const goToPrevious = () => { startTransition(() => { addTransitionType('gallery-slide-right'); setCurrentIndex(prev => (prev - 1 + images.length) % images.length); }); }; const goToNext = () => { startTransition(() => { addTransitionType('gallery-slide-left'); setCurrentIndex(prev => (prev + 1) % images.length); }); }; return ( <ViewTransition enter={{ 'gallery-slide-left': 'image-slide-in-from-right', 'gallery-slide-right': 'image-slide-in-from-left' }} > <div className="gallery"> <img src={images[currentIndex].src} alt="" /> <button onClick={goToPrevious}>← Назад</button> <button onClick={goToNext}>Вперёд →</button> </div> </ViewTransition> ); }Заметь, как addTransitionType вызывается внутри startTransition - это критично. Браузер должен знать тип анимации до того, как произойдёт рендер. Если ты вызовешь addTransitionType снаружи или после setState, анимация не применится.
Воты основные правила при работе со startTransition:
- Всегда оборачивай setState в startTransition, если хочешь, чтобы анимация сработала
- addTransitionType должен быть внутри startTransition, перед изменением состояния
- Избегай flushSync - если синхронно обновишь состояние, браузер не сможет координировать анимацию
- useEffect срабатывает после анимации - если нужны побочные эффекты, они будут после того, как переход завершится
Кастомные CSS-анимации
Дефолтная анимация - кросс-фейд, что часто скучновато. Чтобы сделать что-то интереснее, нужна CSS. Но не обычная CSS-анимация на классах, а анимация самих ::view-transition-old и ::view-transition-new псевдо-элементов.
@keyframes slide-in-from-right { from { opacity: 0; transform: translateX(30px); } to { opacity: 1; transform: translateX(0); } } @keyframes slide-out-to-left { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(-30px); } } /* Применяем к новым элементам */ ::view-transition-new(image-slide-in-from-right) { animation: slide-in-from-right 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; } /* Применяем к старым элементам */ ::view-transition-old(image-slide-in-from-right) { animation: slide-out-to-left 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }Ключевой момент: ты не менял классы в компоненте. Просто описал анимации для конкретных типов переходов, и браузер применил их автоматически. Нет никакого
className={isAnimating ? 'task-enter' : ''}- вся логика в CSS.Вот какие псевдо-элементы и приёмы стоит знать:
::view-transition-old(name)- анимирует старое состояние элемента::view-transition-new(name)- анимирует новое состояние- Duration и timing - используй
animation-durationиanimation-timing-functionдля тонкой настройки - Combine с transform - трансформы отрисовываются намного эффективнее, чем left/top
- opacity для затухания - простой способ сделать переход мягче
Типичный набор анимаций для todo-листа: слайды для появления/исчезновения, пульс для обновления статуса, скейл для выделения. Всё это описывается в CSS, а React просто управляет типом анимации.
Практические примеры: список задач
Давай соберём полный компонент списка задач со всеми фишками: добавление, удаление, переполнение статуса, и все это с красивыми анимациями.
import React, { useState, startTransition } from 'react'; import { ViewTransition, addTransitionType } from 'react'; import './TaskList.css'; function TaskList() { const [tasks, setTasks] = useState([]); const [input, setInput] = useState(''); const handleAddTask = () => { if (!input.trim()) return; startTransition(() => { addTransitionType('task-add'); setTasks(prev => [ ...prev, { id: Date.now(), title: input, completed: false } ]); setInput(''); }); }; const handleToggleTask = (id) => { startTransition(() => { addTransitionType('task-update'); setTasks(prev => prev.map(t => t.id === id ? { ...t, completed: !t.completed } : t ) ); }); }; const handleDeleteTask = (id) => { startTransition(() => { addTransitionType('task-remove'); setTasks(prev => prev.filter(t => t.id !== id)); }); }; return ( <div className="todo-wrapper"> <h1>Мои задачи</h1> <div className="input-group"> <input type="text" value={input} onChange={e => setInput(e.target.value)} onKeyPress={e => e.key === 'Enter' && handleAddTask()} placeholder="Что нужно сделать?" /> <button onClick={handleAddTask}>Добавить</button> </div> <ul className="task-list"> {tasks.map(task => ( <li key={task.id}> <ViewTransition enter={{ 'task-add': 'task-slide-in', 'task-update': 'task-pulse' }} exit={{ 'task-remove': 'task-slide-out' }} > <div className={`task-item ${task.completed ? 'completed' : ''}`}> <input type="checkbox" checked={task.completed} onChange={() => handleToggleTask(task.id)} /> <span className="task-title">{task.title}</span> <button className="delete-btn" onClick={() => handleDeleteTask(task.id)} > × </button> </div> </ViewTransition> </li> ))} </ul> </div> ); } export default TaskList;А вот CSS, который делает это всё красивым:
@keyframes task-slide-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } @keyframes task-slide-out { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-10px) scale(0.95); } } @keyframes task-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.02); } } ::view-transition-new(task-add) { animation: task-slide-in 0.4s ease-out forwards; } ::view-transition-old(task-remove) { animation: task-slide-out 0.4s ease-in forwards; } ::view-transition-new(task-update) { animation: task-pulse 0.5s ease-in-out forwards; } .task-list { list-style: none; padding: 0; margin: 1rem 0; } .task-item { display: flex; align-items: center; gap: 1rem; padding: 1rem; background: #f5f5f5; border-radius: 8px; margin-bottom: 0.5rem; } .task-item.completed .task-title { text-decoration: line-through; color: #999; }Всё работает просто: новая задача появляется со слайдом снизу, обновление статуса вызывает лёгкий пульс, удаление - скользящий выход. Никакие классы не меняются динамически, никакой сложной логики в React. Только ViewTransition, startTransition и CSS.
Когда использовать, когда не использовать
ViewTransition API - мощный инструмент, но не волшебная палочка. Есть сценарии, где его использование имеет смысл, а есть, где это просто оверкилл.
Используй ViewTransition, если:
- Ты переходишь между значительными изменениями в DOM (удаление/добавление блоков элементов)
- Хочешь контролировать анимацию направленно (разные анимации для разных действий)
- Нужна гладкость без сложной синхронизации с React-рендером
- Ты вёрстаешь список, галерею или карусель с переходами
- Есть требования к Core Web Vitals (браузерные анимации обычно быстрее CSS-in-JS)
Избегай, если:
- Нужны микро-анимации на отдельных пиксель-уровневых элементах (используй Framer Motion или CSS)
- Анимация зависит от постоянного ввода пользователя (например, скроллирование, драг)
- Поддержка старых браузеров критична (API довольно новый)
- Логика анимации экстремально сложная и требует десятков состояний
Какие браузеры поддерживают:
- Chrome/Edge - полная поддержка с версии ~118
- Firefox - экспериментальная поддержка
- Safari - появляется постепенно
- Старые браузеры - просто игнорируют API и рендерят без анимации
Поэтому всегда добавляй fallback: проверь наличие
document.startViewTransitionперед использованием.Что помнить о производительности
View Transitions API оптимизирован на уровне браузера, но это не значит, что можно рендерить 500 элементов за раз. Браузер всё ещё должен:
- Захватить снимок текущего состояния (paint)
- Отрисовать новое состояние (paint)
- Создать анимацию между ними (composite)
Если элементов слишком много или DOM слишком сложный, даже встроенная оптимизация не спасёт. Вот что реально помогает:
- Виртуализируй списки - используй react-window или similar, если элементов 100+
- Разбивай на регионы - не оборачивай весь список в один ViewTransition, если только часть меняется
- Минимизируй перерисовки - используй React.memo, если элементы не меняются
- Тестируй на реальных устройствах - девелопер-машина с 32GB памяти не показывает реальное положение дел
Основное правило: ViewTransition экономит на синхронизации (тебе не нужно писать сложный код для управления временем), но не экономит на рендере. Если рендер медленный, анимация тоже будет медленной.
Интеграция с React Router
Если ты используешь React Router, хорошая новость: там уже встроена поддержка View Transitions. Просто добавь
viewTransitionпроп на Link или Form, и маршрут будет переходить с анимацией.import { Link, useViewTransitionState } from 'react-router-dom'; function ImageCard({ id, src, title }) { // Hook предоставляет состояние переходит ли сейчас const isTransitioning = useViewTransitionState(`/image/${id}`); return ( <Link to={`/image/${id}`} viewTransition> <div className="image-card" style={{ viewTransitionName: isTransitioning ? `image-${id}` : 'none' }} > <img src={src} style={{ viewTransitionName: isTransitioning ? `image-${id}-img` : 'none' }} /> <p style={{ viewTransitionName: isTransitioning ? `image-${id}-title` : 'none' }}> {title} </p> </div> </Link> ); }Вотвот так это вообще просто. Router сам вызывает
startViewTransition, тебе нужно только указать, какие элементы участвуют в анимации через viewTransitionName. Можно использовать useViewTransitionState хук, чтобы узнать, сейчас ли происходит переход в нужный маршрут.Когда API будет везде
Safari и Firefox постепенно добавляют поддержку View Transitions API, но универсальной поддержки ещё нет. На момент 2026 года API работает везде, где нужно, но ставить на него как на единственное решение ещё рано для legacy-проектов.
Реальная история: компании, которые перешли на View Transitions, экономят месяцы на разработке анимаций переходов и получают лучше результаты в Core Web Vitals. Это работает. Код становится проще, браузеры оптимизируют рендер лучше.
Но есть нюанс - если проект должен работать в Internet Explorer или старых версиях Safari, придётся либо добавлять полифилл (и теряется смысл оптимизации), либо использовать обычные CSS-анимации как fallback. Правило: всегда проверяй
document.startViewTransitionперед использованием.Что стоит помнить дальше: это не последняя версия API. React и браузеры будут добавлять новые фишки - например, лучшую интеграцию с Suspense, улучшенные колбэки для контроля анимации, поддержка более сложных сценариев.
View Transitions API - это не просто красивые анимации. Это способ писать анимированные интерфейсы с тем же уровнем простоты, что и обычный React-код. Можно сосредоточиться на логике приложения, а не на синхронизации CSS с JavaScript. И это уже сейчас меняет как разработчики пишут фронтенд.
Здравствуйте! Похоже, вас заинтересовала эта беседа, но у вас ещё нет аккаунта.
Надоело каждый раз пролистывать одни и те же посты? Зарегистрировав аккаунт, вы всегда будете возвращаться на ту же страницу, где были раньше, и сможете выбирать, получать ли уведомления о новых ответах (по электронной почте или в виде push-уведомлений). Вы также сможете сохранять закладки и ставить лайки постам, чтобы выразить свою благодарность другим участникам сообщества.
С вашими комментариями этот пост мог бы стать ещё лучше 💗
Зарегистрироваться Войти© 2024 - 2026 ExLends, Inc. Все права защищены.