🔍

React 18. startTransition

React 18. startTransition

Некоторые обновления пользовательского интерфейса должны выполняться как можно быстрее (ввод текста в поле ввода, выбор значения из раскрывающегося списка), в то время как другие могут иметь более низкий приоритет (фильтрация списка).

До сих пор React не предоставлял инструмента для приоритизации обновлений пользовательского интерфейса.

В этом посте вы узнаете, как использовать и зачем нужен хук useTransition().

Какую проблему решает эта фича?

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

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

import React from 'react'

const [inputValue, setInputValue] = React.useState('')

const handleChangeInput = (e) => {
  setInputValue(e.target.value)
}

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

Концептуально проблема в том, что необходимо выполнить два разных обновления. Первое обновление — это срочное обновление, чтобы изменить значение поля ввода и, возможно, некоторый пользовательский интерфейс вокруг него. Второе — менее срочное обновление, показывающее результаты поиска.

import React from 'react'

const [inputValue, setInputValue] = React.useState('')
const [searchQuery, setSearchQuery] = React.useState('')

const handleChangeInput = (e) => {
  // Urgent: Show what was typed
  setInputValue(e.target.value)

  // Not urgent: Show the results
  setSearchQuery(e.target.value)
}
import React from 'react'

const [inputValue, setInputValue] = React.useState('')
const [searchQuery, setSearchQuery] = React.useState('')
const [isPending, startTransition] = React.useTransition()

const handleChangeInput = () => {
  setInputValue(e.target.value)

  startTransition(() => setSearchQuery(e.target.value))
}

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

До React 18 все обновления рендерились сразу. Это означает, что два указанных выше состояния по-прежнему будут отображаться одновременно и по-прежнему будет блокировать взаимодействие пользователя с интерфейсом до тех пор, пока все не будет отрисовано. Чего нам не хватает, так это способа сообщить React, какие обновления являются срочными, а какие нет.

Как помогает startTransition?

Новый API startTransition решает эту проблему, давая вам возможность отмечать обновления как «переходы»:

import React from 'react'

const [inputValue, setInputValue] = React.useState('')
const [searchQuery, setSearchQuery] = React.useState('')
const [isPending, startTransition] = React.useTransition()

const handleChangeInput = () => {
  // Urgent: Show what was typed
  setInputValue(e.target.value)

  // Mark any state updates inside as transitions
  startTransition(() => {
    // Transition: Show the results
    setSearchQuery(e.target.value)
  })
}

Обновления, заключенные в startTransition, обрабатываются как несрочные и будут прерваны, если поступят более приоритетные обновления, такие как щелчки или нажатия клавиш. Если переход прерывается пользователем (например, путем ввода нескольких символов в строке), React дает выполнить устаревшую работу по рендерингу, которая не была завершена, и рендерить только последнее обновление.

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

Что такое переход?

Обновление состояния классифицируются на две категории:

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

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

Например, когда вы выбираете фильтр в раскрывающемся списке, вы ожидаете, что сама кнопка фильтра немедленно отреагирует при нажатии. Однако результаты выдачи фильтров могут меняться отдельно. Небольшая задержка будет незаметной и часто ожидаемой. И если вы снова измените фильтр до того, как результаты будут отрисованы, то вы ожидаете увидеть последние результаты.

В типичном приложении React большинство обновлений концептуально являются обновлениями перехода. Но по причинам обратной совместимости переходы не требуются. По умолчанию React 18 по-прежнему обрабатывает обновления как срочные, и вы можете пометить обновление как переход, заключив его в startTransition.

Чем он отличается от setTimeout?

Распространенное решение вышеуказанной проблемы — обернуть второе обновление в setTimeout:

import React from 'react'

const [inputValue, setInputValue] = React.useState('')
const [searchQuery, setSearchQuery] = React.useState('')
const [isPending, startTransition] = React.useTransition()

const handleChangeInput = (e) => {
  // Show what you typed
  setInputValue(e.target.value)

  // Show the results
  setTimeout(() => {
    setSearchQuery(e.target.value)
  })
}

Это задержит второе обновление до тех пор, пока не будет выполнено первое обновление. Throttling и debouncing — общие варианты этой техники.

Одно важное отличие состоит в том, что startTransition не запланирован на более позднее время, как setTimeout. Он выполняется немедленно. Функция, переданная в startTransition, выполняется синхронно, но любые обновления внутри нее помечаются как «переходы». React будет использовать эту информацию позже при обработке обновлений, чтобы решить, как отображать обновление. Это означает, что мы начинаем рендеринг обновления раньше, чем если бы оно было заключено в setTimeout. На быстром устройстве между двумя обновлениями будет очень небольшая задержка. На медленном устройстве задержка будет больше, но пользовательский интерфейс останется отзывчивым.

Еще одно важное отличие заключается в том, что обновление большого экрана внутри setTimeout по-прежнему блокирует страницу сразу после истечения тайм-аута. Если пользователь все еще набирает текст или взаимодействует со страницей, когда срабатывает setTimeout, он все равно будет заблокирован от взаимодействия со страницей. Но обновления состояния, отмеченные startTransition, прерываются, поэтому они не блокируют страницу. Они позволяют браузеру обрабатывать события в небольших промежутках между отображением различных компонентов. Если пользовательский ввод изменится, React не будет продолжать рендеринг того, что пользователю больше не интересно.

В заключении, поскольку setTimeout просто задерживает обновление, отображение индикатора загрузки требует написания асинхронного кода, который часто бывает нестабильным. При использовании переходов React может отслеживать состояние ожидания (isPending) для вас, обновляя его на основе текущего состояния перехода и давая вам возможность отображать информацию о загрузке, пока пользователь ждет.

Что мне делать, пока ожидается переход?

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

import { useTransition } from 'react'

const [isPending, startTransition] = useTransition()

Пока переход находится в состоянии ожидания, то значение isPending true, что позволяет отображать загрузку, пока пользователь ждет:

{isPending && <Spinner />}

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

Почему бы просто не написать более быстрый код?

Написание более быстрого кода и избежание ненужных отрисовок по-прежнему являются хорошими способами оптимизации производительности. Переходы дополняют это. Они позволяют поддерживать отзывчивость пользовательского интерфейса даже во время значительных визуальных изменений — например, при отображении нового экрана. Это сложно оптимизировать с помощью существующих стратегий. Даже когда нет лишнего рендеринга, переходы по-прежнему обеспечивают лучший пользовательский опыт, чем использование каждого отдельного обновления как срочное.

Где я могу это использовать?

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

Заключение

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

Начиная с React 18 появился хук useTransition(), который помогает нам отмечать обновления пользовательского интерфейса как низкоприоритетные, что особенно полезно для тяжелых несрочных обновлений.

Читать ещё:

React 18. Пакетная обработка (Автоматическая группировка изменений)

React 18. Пакетная обработка (Автоматическая группировка изменений)

React 18. createRoot

React 18. createRoot