Содержание
В React v18 появились два новых хука - useTransition() и useDeferredValue() - помогающие расставить приоритеты при обновлении пользовательского интерфейса на стороне клиента. Теперь вы можете явно отдавать приоритет определенному пользовательскому взаимодействию, которое является вялым и медленным, по сравнению с другими обновлениями пользовательского интерфейса.
Такое поведение гарантирует, что все тяжелые обновления пользовательского интерфейса будут происходить плавно, а менее значимые обновления пользовательского интерфейса могут выполняться параллельно или после завершения более приоритетных обновлений. В этой статье мы рассмотрим, как использовать хуки useTransition() и useDeferredValue() в вашем следующем проекте.
Зачем вообще отдавать приоритет обновлениям пользовательского интерфейса?
Приоритетность обновлений пользовательского интерфейса - один из способов оптимизации производительности в React-приложении. При работе со сложными обновлениями состояния вы, скорее всего, столкнетесь с ситуациями, когда определенное обновление пользовательского интерфейса происходит медленно из-за интенсивных вычислений, выполняемых на стороне клиента.
Например, представьте, что у вас на экране отображается список из 10 000 товаров, и вы хотите реализовать функцию поиска по названию продукта.
В идеале вы никогда не должны выводить на экран сразу 10 000 элементов, а использовать какую-либо технику пагинации или ленивой загрузки. Но для примера предположим, что все элементы выводятся на экран одновременно.
Теперь, когда вы реализуете функцию поиска и привязываете ее к событию onChange, текст, который вы вводите в поле ввода, будет довольно сильно тормозить. Это связано с тем, что каждое нажатие клавиши отвечает за обновление и отображение большого количества продуктов в списке.
Это идеальный случай, когда приоритет отдается обновлению пользовательского интерфейса при нажатии клавиш, а не рендерингу списков, расположенных ниже. Вы хотите обеспечить отсутствие задержки при вводе текста в текстовом поле, но задержка в несколько микросекунд при отрисовке списка вполне допустима с точки зрения пользовательского опыта.
Как помогает крючок useTransition()?
React v18 решает эту проблему в нашем примере выше, предоставляя уникальный хук под названием useTransition. Вы можете просто использовать этот хук, чтобы обернуть событие, отвечающее за обновление нажатия клавиш в текстовом поле пользовательского интерфейса.
Хук useTransition возвращает массив с двумя переменными:
const [isPending, startTransition] = useTransition();
Первая переменная - это булево значение, которое сообщает, ожидается ли неблокируемое обновление пользовательского интерфейса.
Вторая переменная - это функция, которая может обернуть ваше обновление состояния для “перехода” - это означает, что конкретный переход имеет более высокий приоритет и будет выполнен как неблокирующее обновление состояния UI.
В примере, описанном выше, у вас есть поле ввода и прикрепленный к нему обработчик события onChange:
function App() {
const [search, setSearch] = useState('');
function handleFilterChange(e) {
setSearch(e.target.value);
}
return <input type="search" onChange={handleFilterChange} />;
}
Вы можете улучшить медлительность ввода, используя хук useTransition следующим образом:
function handleFilterChange(e) {
startTransition(() => {
setSearch(e.target.value);
});
}
Теперь обновление пользовательского интерфейса setSearch(e.target.value) будет рассматриваться как переход.
Вы всегда можете использовать первую переменную, которую предоставляет хук useTransition, чтобы проверить, ожидает ли переход. Переменная isPending может быть позже использована для отображения ожидающего перехода в вашем главном компоненте App.
Вот полный код, реализующий хук useTransition для улучшения пользовательского опыта при вводе текста в поле ввода:
import { useState, useTransition } from 'react';
import List from './List';
- Создайте фиктивный список из 10000 элементов, имитирующий большое количество товаров.
function dummyList() {
const items = [];
for (let i = 0; i < 10000; i++) {
items.push(`Item ${i + 1}`);
}
return items;
}
const list = dummyList();
function filterItems(search) {
if (!search) {
return list;
}
return list.filter((product) => product.includes(filterTerm));
}
создайте функциональный компонент List, который впоследствии будет использоваться для отображения списка элементов в компоненте App.
function List({ items }) {
return (
<div>
{items.map((item, index) => (
<Fragment key={index}>
<div>{item}</div>
</Fragment>
))}
</div>
);
}
function App() {
const [isPending, startTransition] = useTransition();
const [search, setSearch] = useState('');
const searchedItems = filterItems(search);
function handleFilterChange(e) {
startTransition(() => {
setSearch(e.target.value); // wrapping setSearch in a transition
});
}
return (
<div>
<input type="search" onChange={handleFilterChange} />
{isPending ? <div>Loading...</div> : null}
<List items={searchedItems} />
</div>
);
}
Соображения по поводу использования useTransition().
Важно отметить, что компонент input является неуправляемым компонентом. Это работает в данном примере, поскольку вы не хотите, чтобы значение состояния синхронизировалось со значением ввода; вместо этого вы хотите, чтобы setSearch(e.target.value) обрабатывался как более приоритетный.
При этом useTransition не может работать с компонентом управляемого ввода, поскольку и вводимое значение, и результат фильтрации будут синхронизированы. Это приведет к той же исходной проблеме - т. е. к вялому поведению из-за большого количества элементов списка.
Сам React считает использование хука useTransition в управляемом компоненте антипаттерном. Приведенный ниже код является антипаттерном и не должен использоваться вообще:
function App() {
const [search, setSearch] = useState('');
function handleFilterChange(e) {
startTransition(() => {
setSearch(e.target.value);
});
}
return (
<div>
{/* ❌ Never do this with controlled component */}
<input type="search" value={search} onChange={handleFilterChange} />
</div>
);
}
Поэтому, хотя хук useTransition может отлично подходить для обработки обновлений состояния, он должен быть крайним средством или использоваться только в случае медленного обновления UI. Это особенно актуально при работе со старыми устройствами с медленными процессорами.
Большинство обновлений пользовательского интерфейса может быть обработано самим React, если все сделано правильно. Существует также другой хук под названием useDeferredValue, который был представлен в React v18, который решает очень похожий набор проблем и может быть использован, если у вас нет контроля над вызовами состояния, как в этом примере:
) Deferring state updates with the
useDeferredValue() Hook
Хук useDeferredvalue, как следует из названия, помогает вам “отложить” обновление состояния.
В приведенном выше примере вы также можете использовать этот хук, если у вас нет контроля над тем, как вызывается состояние списка, а вы можете манипулировать только компонентом List. В этом случае вы можете передать реквизит items хуку useDeferredValue и отобразить его вместо прямого обращения к items, например, так:
function List({ items }) {
const deferredItems = useDeferredValue(items);
return (
<div>
{deferredItems.map((item, index) => (
<Fragment key={index}>
<div>{item}</div>
</Fragment>
))}
</div>
);
}
Это обеспечит быстрое и оперативное обновление вводимых данных, в то время как список будет обновляться довольно долго. Вы могли видеть такое поведение, если использовали дебаунсинг в поисковой функциональности. Этот хук ведет себя точно так же, но откладывает обновление пользовательского интерфейса списка на каждое нажатие клавиши.
Если в целом вы контролируете вызовы состояния, то лучше использовать хук useTransition. В противном случае вы всегда можете отложить обновление пользовательского интерфейса из самого компонента List, что в некотором роде напоминает метод debounce.
Не следует смешивать и сочетать хуки useDeferredValue и useTransition, так как они оба решают одну и ту же проблему.
Однако вы, вероятно, захотите использовать useDeferredValue вместе с дебаунсингом или дросселированием, что может еще больше улучшить пользовательский опыт и сэкономить несколько сетевых вызовов, пока пользователь взаимодействует с полем ввода.
Заключение
Хуки useTransition и useDeferredValue могут быть очень полезны для решения проблемы медленных и нестабильных обновлений пользовательского интерфейса, которые вызваны либо низкой производительностью процессора, либо внешними факторами, такими как сам API.
Всегда помните, что приоритетное обновление пользовательского интерфейса должно быть крайним средством. В первую очередь вы всегда должны стараться разработать производительный пользовательский интерфейс с использованием хороших практик работы с кодом и паттернов React.