Menu

Хук useTransition в React: отзывчивый интерфейс

useTransition помечает обновление состояния как несрочное, чтобы React мог сохранять отзывчивость страницы, пока в фоне рендерится медленная часть. isPending, startTransition, асинхронные переходы в React 19 и почему это не debounce.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

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

Нажмите posts, а затем, до появления списка, нажмите contact. Contact появляется сразу: React выбросил наполовину готовый рендер posts. Теперь замените startTransition(() => { setTab(next); }) на простой setTab(next); и повторите: после клика по posts страница замирает примерно на 300 мс, а ваш клик по contact ждёт, пока posts не закончатся.

Синтаксис

const [isPending, startTransition] = useTransition();

startTransition(() => {
    setTab(next);
});
  • startTransition принимает функцию и сразу её выполняет. Все обновления состояния внутри помечаются как переходы.
  • isPending равен true с момента начала перехода до тех пор, пока React не закончит его рендер и не зафиксирует результат.

useTransition не принимает аргументов. Переданная функция выполняется синхронно; переход касается того, как React рендерит итоговое обновление, а не того, когда выполняется ваша функция.

Что значит «несрочное»

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

React рендерит переход небольшими частями и между компонентами проверяет, не пришло ли что-то срочное. Если пришло, React приостанавливает или бросает переход, обрабатывает срочное обновление, а затем снова начинает переход с последним состоянием. Пока переход рендерится, на экране остаётся предыдущий вид; React не показывает наполовину собранный результат. Поэтому пример продолжает показывать текст about с подсказкой Loading рядом с кнопками, пока posts не будут готовы.

Прерывание происходит между компонентами. Один компонент, который блокирует на 300 мс, нельзя разрезать пополам, поэтому медленная работа выше разнесена по 300 маленьким компонентам SlowPost.

Подсказка об ожидании

isPending это место для индикатора загрузки, который не заменяет текущее содержимое: спиннера рядом с кнопкой, приглушённого текста, надписи «Saving...». В первом примере он показывает Loading... рядом с вкладками. Его можно использовать и для стилизации содержимого:

<div style={{ opacity: isPending ? 0.6 : 1 }}>{content}</div>

Фильтрация большого списка во время ввода

Собственное значение поля в переход поместить нельзя. Контролируемый <input> должен получать новое значение в том же срочном обновлении, иначе символы теряются. Вместо этого держите два значения состояния: текст поля (срочно) и запрос, который использует список (переход).

Быстро наберите 123. Поле никогда не отстаёт. Console всё равно выводит по строке rendering results на каждое нажатие клавиши, потому что Results выводит сообщение, как только React начинает его рендерить, но списки для 1 и 12 так и не доходят до экрана: каждое новое нажатие прерывало текущий рендер, и React начинал заново с последним запросом. Results намеренно обёрнут в memo: без него срочный рендер на каждое нажатие клавиши также рендерил бы медленный список со старым запросом, и выгода пропала бы.

startTransition без хука

react также экспортирует startTransition как обычную функцию. Он помечает обновления как переходы так же, как хук, но isPending вы не получаете. Используйте его там, где нельзя вызвать хук, например в библиотеке данных или роутере вне какого-либо компонента.

import { startTransition } from 'react';

function navigate(url) {
    startTransition(() => {
        setCurrentUrl(url);
    });
}

Асинхронные переходы в React 19

Начиная с React 19 передаваемая функция может быть async. Тогда isPending остаётся true всё время выполнения функции, включая ожидание, и это простой способ показать состояние ожидания во время запроса. Поддельная saveName ниже заменяет настоящий запрос.

Обратите внимание на второй startTransition вокруг setSaved. После await React теряет переход из виду, поэтому обновление состояния после него нужно обернуть снова, чтобы оно считалось частью перехода. Функцию, переданную таким образом, React называет действием (action); на том же механизме построены useActionState и действия форм.

Переход это не debounce

Debounce ждёт фиксированное время после последнего нажатия клавиши, прежде чем что-то делать. Переход не ждёт. Он начинает рендеринг сразу, а если приходит более новое обновление, бросает старую работу. На быстром устройстве результат появляется вообще без задержки; на медленном React делает между нажатиями столько, сколько может. Чего переход не умеет, так это уменьшать число сетевых запросов: если каждое нажатие клавиши запускает fetch, применяйте debounce к fetch.

Переходы и Suspense

Переходы также меняют поведение границы Suspense. Обычно, когда компонент внутри границы приостанавливается (загружается ленивый компонент или читаются данные через use), React заменяет содержимое запасным вариантом. Если обновление, вызвавшее приостановку, было переходом, React вместо этого сохраняет содержимое, которое уже на экране, а isPending остаётся true, пока новое содержимое не будет готово. Так роутеры избегают мелькания спиннера на всю страницу при каждой навигации: они оборачивают навигацию в переход, и старая страница остаётся видимой, пока следующая не сможет отрендериться. Содержимое, которое ещё ни разу не показывалось, по-прежнему использует запасной вариант, поэтому впервые показанная граница ведёт себя как обычно.

Что не работает

  • Обновления внутри таймаута. startTransition(() => setTimeout(() => setTab(next), 100)) не помечает обновление, потому что оно выполняется после того, как startTransition уже вернул управление. Вместо этого поместите startTransition внутрь таймаута.
  • Один медленный компонент. React может прерывать только между компонентами, поэтому один компонент, который долго блокирует, всё равно блокирует.
  • Управление полем ввода. Как сказано выше, собственное состояние поля остаётся срочным.

Для случая, когда сеттер состояния вам не принадлежит и вы получаете только значение, например пропс, то же поведение даёт useDeferredValue.

Часто задаваемые вопросы

Что делает useTransition?

Он даёт startTransition, который помечает обновления состояния внутри как несрочные, и isPending, равный true, пока это обновление ещё рендерится. React может прервать несрочный рендер, чтобы сначала обработать клики и ввод.

Чем useTransition отличается от startTransition?

Оба помечают обновления как переходы. Хук useTransition также даёт isPending для индикатора загрузки. Отдельный startTransition, импортированный из react, работает вне компонентов, но флага ожидания у него нет.

Можно ли использовать useTransition для текстового поля?

Не для собственного значения поля. Контролируемое поле должно обновляться синхронно, иначе ввод ломается. Оставьте состояние поля срочным, а дорогое обновление, которое от него зависит, поместите в переход или используйте useDeferredValue.

useTransition это то же самое, что debounce?

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

Может ли startTransition принимать асинхронную функцию?

Да, начиная с React 19. isPending остаётся true, пока асинхронная функция не закончится. Обновления состояния после await нужно обернуть ещё в один вызов startTransition, чтобы они считались переходом.

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ