Перейти к содержанию
  • Лента
  • Категории
  • Последние
  • Метки
  • Популярные
  • Пользователи
  • Группы
Свернуть
exlends
Категории
  1. Главная
  2. Категории
  3. Фронтенд
  4. React 19 ViewTransition: плавные анимации списков задач

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

Запланировано Прикреплена Закрыта Перенесена Фронтенд
react 19анимацииviewtransition
1 Сообщения 1 Постеры 5 Просмотры
  • Сначала старые
  • Сначала новые
  • По количеству голосов
Ответить
  • Ответить, создав новую тему
Авторизуйтесь, чтобы ответить
Эта тема была удалена. Только пользователи с правом управления темами могут её видеть.
  • hannadevH Не в сети
    hannadevH Не в сети
    hannadev
    написал отредактировано
    #1

    Обложка: React 19 ViewTransition API: плавные анимации переходов списков задач с startTransition и кастомными типами анимаций

    Фронтенд развивается в сторону более гладких и приятных взаимодействий, и 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. И это уже сейчас меняет как разработчики пишут фронтенд.

    1 ответ Последний ответ
    0

    Здравствуйте! Похоже, вас заинтересовала эта беседа, но у вас ещё нет аккаунта.

    Надоело каждый раз пролистывать одни и те же посты? Зарегистрировав аккаунт, вы всегда будете возвращаться на ту же страницу, где были раньше, и сможете выбирать, получать ли уведомления о новых ответах (по электронной почте или в виде push-уведомлений). Вы также сможете сохранять закладки и ставить лайки постам, чтобы выразить свою благодарность другим участникам сообщества.

    С вашими комментариями этот пост мог бы стать ещё лучше 💗

    Зарегистрироваться Войти

    Категории

    • Главная
    • Новости
    • Фронтенд
    • Бекенд
    • Языки программирования

    Контакты

    • Сотрудничество
    • info@exlends.com

    © 2024 - 2026 ExLends, Inc. Все права защищены.

    Политика конфиденциальности
    • Войти

    • Нет учётной записи? Зарегистрироваться

    • Войдите или зарегистрируйтесь для поиска.
    • Первое сообщение
      Последнее сообщение
    0
    • Лента
    • Категории
    • Последние
    • Метки
    • Популярные
    • Пользователи
    • Группы