React

useTransition: неблокирующие обновления

Вкладки с тяжёлым содержимым: пользователь кликает по второй вкладке, под которой рендерится огромный график, и интерфейс замирает на полсекунды - сама кнопка вкладки даже не успевает подсветиться как нажатая. Действие ощущается сломанным. Конкурентная модель уже дала идею: разделить срочное и несрочное. useTransition - первый инструмент, который воплощает её на практике. Он позволяет сказать React прямо: 'переключение вкладки срочное, а тяжёлая перерисовка под ней - нет, и она не должна мешать остальному интерфейсу отвечать'.

  • Переключение вкладок и фильтров над тяжёлым контентом: клик откликается мгновенно, контент догружается следом
  • Живой поиск: поле остаётся плавным, пока React в фоне перестраивает большой список результатов
  • Навигация в SPA: переход помечают транзицией, чтобы старый экран оставался интерактивным до готовности нового
  • Индикаторы загрузки через isPending: ненавязчивое затемнение области, пока готовится несрочное обновление
  • React 18+ (2022): useTransition - часть стабильного конкурентного API, активно используется в роутерах и библиотеках данных

Предварительные знания

  • Модель срочных и несрочных обновлений из урока про идею конкурентного рендеринга
  • Понимание, что начатый несрочный рендер React может прервать ради срочного
  • Базовая работа с useState и обработчиками событий

От Concurrent Mode к стабильному API

Долгое время приоритезация обновлений жила в экспериментальных сборках под названием Concurrent Mode и требовала особого включения. Команда искала API, который не заставлял бы переписывать приложение целиком. Ответом стали точечные хуки, активирующие конкурентность только там, где она нужна. В React 18 (март 2022) useTransition вышел стабильно вместе с новым корневым API createRoot. Его задача узкая и ясная: пометить конкретное обновление состояния как несрочное, чтобы React мог отложить связанный с ним тяжёлый рендер, не блокируя срочные действия пользователя. К 2026 году хук встроен в роутеры и библиотеки работы с данными как штатный способ держать интерфейс отзывчивым.

Что делает транзиция

useTransition даёт способ пометить обновление состояния как несрочное. Хук возвращает массив из двух элементов: булев флаг isPending и функцию startTransition. Любое обновление состояния, вызванное внутри коллбэка startTransition, React считает несрочным - транзицией. Такой рендер React готов прервать, отложить и при необходимости отбросить ради срочной работы, как описывала модель конкурентности.

Ключевая идея - разделить в одном обработчике два разных обновления. То, что должно примениться немедленно (значение поля ввода, визуальное состояние нажатой кнопки), оставляют срочным, вне startTransition. То, что порождает тяжёлый рендер (перестроить список или график по новому выбору), оборачивают в startTransition. Тогда срочное обновление применится сразу, а несрочное React выполнит, не мешая отзывчивости.

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

Что именно делает обновление состояния, обёрнутое в startTransition?

isPending и индикация прогресса

Второй элемент пары, isPending, отвечает на вопрос 'идёт ли сейчас транзиция'. Пока React выполняет отложенный рендер в фоне, isPending равен true, а после завершения становится false. Это даёт честную обратную связь: интерфейс может показать, что новое содержимое готовится, не блокируя при этом текущее.

Тонкость в том, что во время транзиции React продолжает показывать прошлый, ещё актуальный результат, а не пустоту или спиннер. Пользователь видит старый список слегка приглушённым, пока в фоне готовится новый, и в момент готовности он плавно сменяется. Это принципиально приятнее, чем привычное мигание 'контент исчез - спиннер - контент появился'.

  • Без транзиции — Тяжёлое обновление блокирует поток. Интерфейс замирает целиком, нажатия не откликаются, индикатор показать негде.
  • С транзицией и isPending — Срочные действия откликаются сразу, старый контент остаётся видимым и приглушается, isPending честно сигналит о подготовке нового.

isPending лучше использовать для лёгкой, неблокирующей индикации: приглушить область, показать тонкую полосу прогресса. Подменять весь контент полноэкранным спиннером на время транзиции - значит выбросить главное преимущество: возможность оставить старый результат видимым и интерактивным.

Что показывает интерфейс во время транзиции, пока isPending равен true?

Когда применять, а когда нет

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

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

Частая ошибка - обернуть в startTransition обновление значения поля ввода. Тогда сам ввод становится несрочным и буквы начинают отставать - получается ровно тот лаг, с которым боролись. В транзицию кладут только производную тяжёлую работу, а обновление поля всегда оставляют срочным.

Стоит помнить и про разделение труда внутри блока конкурентности. useTransition работает, когда у разработчика под рукой само обновление состояния, которое он запускает. Если же тяжёлый рендер зависит от пропса или значения, которое приходит извне и не оборачивается в setState, удобнее другой инструмент - useDeferredValue из следующего урока. Оба опираются на одну модель приоритетов, но цепляются за разные точки.

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

Почему нельзя оборачивать обновление значения самого поля ввода в startTransition?

Связь с другими темами

useTransition - один из двух главных инструментов конкурентности:

  • Идея конкурентности — Даёт модель срочное/несрочное, которую useTransition применяет к обновлениям состояния
  • useDeferredValue — Решает ту же задачу с другой стороны: откладывает значение, а не само обновление
  • Мемоизация — Помогает дорогому потомку не пересчитываться зря, усиливая эффект транзиции

Итог

  • useTransition помечает обновление состояния как несрочное (транзицию), чтобы тяжёлый рендер от него не блокировал срочный ввод
  • Хук возвращает пару: флаг isPending и функцию startTransition, в которую оборачивают несрочное обновление
  • Срочные обновления (поле, кнопка) делают как обычно, а производную тяжёлую работу кладут внутрь startTransition
  • isPending показывает, что транзиция идёт, и позволяет деликатно отметить область как обновляющуюся, не блокируя её
  • Транзиция не ускоряет рендер - она снижает его приоритет, позволяя React прервать его ради срочных действий

Связанные уроки

  • rc-24-concurrent-intro — useTransition - практическое применение модели срочных и несрочных обновлений, разобранной в уроке про идею конкурентности
  • rc-26-usedeferredvalue — useDeferredValue решает близкую задачу, но откладывает значение, а не обновление состояния - прямое сравнение двух подходов
  • rc-19-memoization — Мемоизация тяжёлого потомка усиливает выигрыш от транзиций, не давая ему пересчитываться зря
useTransition: неблокирующие обновления

0

1

Войти