
Веб-приложения сегодня требуют всё большей интерактивности, отзывчивости и быстродействия. В ответ на это команда React постоянно совершенствует инструментарий, позволяющий нам тонко управлять рендерингом и пользовательским опытом. Если вы работали только с классическими методами оптимизации вроде useMemo, useCallback, мемоизации компонент через React.memo и другими известными приёмами, то вас могут заинтересовать следующие хуки:
useTransition- устанавливает приоритеты рендеринга, разделяя обновления на критические и фоновые.useDeferredValue- откладывает обновление тяжёлых значений, чтобы интерфейс не фризился при вводе данных.useOptimistic- помогает реализовать оптимистичные обновления "из коробки".
В этой статье мы разберём ключевые идеи каждого из этих хуков и рассмотрим практические примеры, чтобы стало ясно, как и когда их применять.
useTransition: приоритеты рендеринга и плавность UI
Общая идея
Когда пользователь совершает действие (например, вводит текст, переключает вкладки, жмёт кнопку), нам может понадобиться выполнить довольно тяжелое обновление состояния: фильтрация большой коллекции, пересчёт сложных данных, перестройка таблицы и т.д. Если всё это произойдёт сразу (с высоким приоритетом), то UI может подвиснуть на секунду - пользователь увидит задержку при нажатии или вводе текста.
В React 18 появился хук useTransition, который позволяет пометить какое-то обновление как "некритичное" или "переходное". При этом ключевые моменты взаимодействия с интерфейсом (клик, ввод) остаются отзывчивыми, а само тяжёлое обновление может происходить чуть позже или в фоновом режиме.
Как пользоваться: базовый пример
В этом примере, при вводе текста, поле ввода обновляется мгновенно, чтобы быть отзывчивым, но фильтрация большого списка (filteredItems) оборачивается в startTransition, что позволяет React "приостанавливать" вычисления, если пользователь вводит много символов подряд. Это помогает избежать задержек в интерфейсе. Индикатор загрузки отображается, пока процесс фильтрации не завершится.
import React, { useState, useTransition } from 'react';
function BigList({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
);
}
export default function App() {
const [text, setText] = useState('');
const [filteredItems, setFilteredItems] = useState([]);
const [isPending, startTransition] = useTransition(); // Хук useTransition
// Допустим, у нас есть большая коллекция
const allItems = Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `Item ${i}`
}));
const handleInputChange = (e) => {
const value = e.target.value;
setText(value);
// Некритичное обновление вынесем в startTransition
startTransition(() => {
const filtered = allItems.filter((item) =>
item.text.toLowerCase().includes(value.toLowerCase())
);
setFilteredItems(filtered);
});
};
return (
<div>
<h1>Список: {filteredItems.length} элементов</h1>
<input
value={text}
onChange={handleInputChange}
placeholder="Поиск по списку..."
/>
{/* Показываем индикатор, если useTransition ещё "в процессе" */}
{isPending && <p>Loading...</p>}
<BigList items={filteredItems} />
</div>
);
}
Как это работает?
При каждом вводе символа мы сразу обновляем
text(чтобы поле ввода было отзывчивым).Но пересчёт большого списка (
filteredItems) оборачиваем вstartTransition(...).Если пользователь вводит много символов подряд быстро, React может приостанавливать вычисления и обновлять список, только когда пользователь притормозит с вводом - чтобы UI оставался плавным.
isPendingговорит, что "переход" ещё идёт, и можно показывать индикатор загрузки.
Какие задачи решает useTransition?
Фильтрация/сортировка больших списков.
Перерисовка сложных компонент (например, карт с множеством объектов).
Плавные анимации при переходе между экранами или вкладками.
Подводные камни и замечания
useTransitionне отменяет само вычисление; оно просто даёт React возможность оптимальнее распределять приоритеты.Если у вас очень тяжёлая логика, может потребоваться дополнительная оптимизация (например, мемоизация или вынесение вычислений на Web Worker).
Не злоупотребляйте
useTransition: если все обновления помечать как "некритичные", то пользователи будут видеть задержки.
useDeferredValue: ленивое обновление больших данных
В чём суть?
useDeferredValue - ещё один хук, представленный в React 18, решает похожую задачу оптимизации. Но здесь мы имеем дело не с разметкой обновления (как в useTransition), а с "двойным" состоянием:
Основное состояние, которое обновляется сразу.
Отложенное состояние, которое может обновляться чуть позже, с меньшим приоритетом.
Это бывает полезно, когда у нас, например, в интерфейсе есть поле ввода и огромный список/таблица, зависящая от этого ввода. Мы хотим, чтобы инпут не тормозил, а обновление списка происходило лениво (deferred).
Пример использования
Здесь при вводе текста поле обновляется сразу, но для компонента SearchResults передается отложенное значение с помощью useDeferredValue. Это позволяет React обновлять список с меньшим приоритетом, предотвращая лишние рендеры при каждом вводе и улучшая производительность, особенно при работе с большими данными.
import React, { useState, useDeferredValue, memo } from 'react';
const SearchResults = memo(function SearchResults({ searchTerm }) {
// Допустим, searchTerm - это уже отложенное значение
const allItems = Array.from({ length: 5000 }, (_, i) => `Item ${i}`);
// Моделируем какую-то тяжёлую фильтрацию
const filteredItems = allItems.filter((item) =>
item.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<ul>
{filteredItems.map((item, idx) => (
<li key={idx}>{item}</li>
))}
</ul>
);
});
export default function App() {
const [inputValue, setInputValue] = useState('');
// "Отложенное" значение на основе текущего inputValue
const deferredValue = useDeferredValue(inputValue);
const handleChange = (e) => {
setInputValue(e.target.value);
};
return (
<div>
<input value={inputValue} onChange={handleChange} placeholder="Поиск..." />
{/*
В компонент SearchResults передаем НЕ inputValue напрямую,
а именно deferredValue, чтобы он мог обновиться с меньшим приоритетом
*/}
<SearchResults searchTerm={deferredValue} />
</div>
);
}
Как это работает?
inputValueменяется мгновенно, и пользователь сразу видит, что поле ввода отражает его действия (без задержки).Но
SearchResultsполучает неinputValue, аdeferredValue. Это значит, что React может подождать подходящий момент для рендера.Так мы избегаем моментальных ререндеров тяжёлых списков при каждом набранном символе.
Отличие от useTransition
useTransition: мы явно оборачиваем какую-то логику (обновление стейта) вstartTransition.useDeferredValue: мы имеем два состояния - основное и отложенное. Просто используемdeferredValueтам, где рендеры могут быть отложены.
Иногда эти хуки можно комбинировать - всё зависит от конкретной задачи.
Зачем нужен useDeferredValue, если есть useDebounce?
Оба хука решают задачи, связанные с производительностью, но делают это разными способами. useDebounce полезен для задержки вызовов функций на основе времени, чтобы предотвратить частые обновления данных, тогда как useDeferredValue предназначен для управления приоритетом обновлений интерфейса. Если задача заключается в том, чтобы не блокировать интерфейс при рендеринге большого количества элементов, но при этом не задерживать обновление самого значения (например, в поле ввода), то useDeferredValue - лучший выбор.
В то время как useDebounce можно использовать для обработки асинхронных операций (например, для уменьшения количества запросов), useDeferredValue лучше подходит для управления рендером и асинхронными обновлениями пользовательского интерфейса.
useOptimistic: оптимистичные обновления без боли
Что такое оптимистичные обновления?
Допустим, у нас есть форма, где пользователь может отправить комментарий или пост. В классическом сценарии мы:
Отправляем запрос на сервер.
Ждём ответа (200 OK).
Только тогда обновляем UI, показывая новый комментарий в списке.
Но если задержка большая, пользователь видит зависание: форма не обновляется, хотя он уже нажал Отправить. Чтобы интерфейс казался шустрее, мы делаем оптимистичное обновление - сразу добавляем комментарий в список как будто запрос сработал, а если что-то пошло не так (сервер вернул ошибку), то откатываем.
Ранее это приходилось вручную реализовывать: хранить временные айдишники, отменять изменения в случае ошибки и т. д. Но в React 19 есть хук useOptimistic, призванный упростить всю эту схему.
Как это выглядит в коде?
В этом примере при отправке нового комментария он сразу добавляется в список, создавая видимость успешной отправки. Если запрос на сервер не удаётся, временный комментарий удаляется, и интерфейс возвращается к исходному состоянию. Это делает интерфейс более отзывчивым, поскольку не нужно ждать ответа от сервера для обновления UI.
import { useOptimistic, useState, useRef } from "react";
async function makeOrder(orderName) {
// мок запроса на сервер
await new Promise((res) => setTimeout(res, 1500));
return orderName;
}
function Kitchen({ orders, onMakeOrder }) {
const formRef = useRef();
async function formAction(formData) {
const orderName = formData.get("orderName");
addOptimisticOrder(orderName);
formRef.current.reset();
await onMakeOrder(orderName);
}
const [optimisticOrders, addOptimisticOrder] = useOptimistic(
orders,
(state, newOrder) => [...state, { orderName: newOrder, preparing: true }]
);
return (
<div>
<form action={formAction} ref={formRef}>
<input type="text" name="orderName" placeholder="Введите заказ!" />
<button type="submit">Заказать</button>
</form>
{optimisticOrders.map((order, index) => (
<div key={index}>
{order.orderName}
{order.preparing ? (
<span> (Готовиться...)</span>
) : (
<span> (Готов!)</span>
)}
</div>
))}
</div>
);
}
export default function App() {
const [orders, setOrders] = useState([]);
async function onMakeOrder(orderName) {
const sentOrder = await makeOrder(orderName);
setOrders((orders) => [...orders, { orderName: sentOrder }]);
}
return <Kitchen orders={orders} onMakeOrder={onMakeOrder} />;
}
Чем это удобно?
useOptimisticупрощает хранение реального и оптимистичного состояния.Нам не нужно вручную делать многоуровневый микс стейта: хук из коробки содержит средства для отката и объединения изменений.
При ошибке запроса мы можем просто откатить оптимистичное действие.
Реальные кейсы, где эти хуки упрощают жизнь
-
Поиск и автодополнение:
useDeferredValueпомогает избежать подвисаний при каждом введённом символе.useTransition- если нужно одновременно показывать живое автодополнение и при этом перерисовывать сложные компоненты.
-
Онлайн-редакторы (текст, графика и пр.):
При масштабировании полотна или при изменениях в большом документе, мы можем использовать переходы, чтобы UI оставался отзывчивым.
-
Мессенджеры и социальные сети:
useOptimisticидеально подходит для отправки сообщений, комментариев, лайков - чтобы пользователь видел быстрый отклик (Сообщение отправлено), а при ошибке мы могли откатить и показать уведомление.
-
Электронная коммерция (корзины, заказы):
Оптимистичное добавление товаров в корзину или оформление заказа с мгновенной реакцией UI.
-
Фильтрация и сортировка больших таблиц, списков:
useDeferredValueиuseTransitionпозволяют не блокировать интерфейс при каждом изменении фильтра.
Когда (и как) не стоит применять эти хуки
Там, где нет больших данных. Если список маленький (двадцать записей) и всё и так работает мгновенно, избыточно добавлять новую логику приоритизации.
Для простых синхронных обновлений. Если задача - просто добавить элемент в массив, и время обновления мизерное,
useTransitionможет быть лишним.useOptimistic: подходит для использования в продакшн-проектах, но в сложных случаях с специфичной логикой обработки ошибок можно рассмотреть альтернативы, такие как React Query или RTK Query, которые также поддерживают оптимистичные обновления.
Итоги
useTransition: Оборачиваем некритические обновления стейта, чтобы рендеры пониженного приоритета не блокировали интерфейс.useDeferredValue: Даём React возможность отложенно обновлять тяжёлое состояние - удобно в связке с формами, поиском и большими списками.useOptimistic: Упрощает реализацию оптимистичных обновлений, когда нужно мгновенно показывать результат действий пользователя, а при сбое - отменять.
В ближайших публикациях я рассмотрю каждый из хуков подробнее:
-
useTransition: продвинутые паттерныКак отменять/перезапускать переходы, несколько параллельных переходов, кейсы с анимациями.
-
useDeferredValue: от теории к практикеРазбор edge-case’ов, замеры производительности, тонкости в сочетании с
useMemoиuseCallback.
-
useOptimistic: реализация сложных сценариевНесколько параллельных оптимистичных обновлений, откаты, интеграция с Redux/RTK Query и т.д.
Надеюсь, эта статья поможет вам улучшить производительность интерфейсов. Даже если в вашем проекте нет потребности в сложной оптимизации, знание хуков useTransition, useDeferredValue и useOptimistic поможет вам быстро улучшить отзывчивость UI в нужный момент.
Дополнительно, если вам интересна тема управления состоянием в React, рекомендую ознакомиться с моей статьёй на Habr, где я подробнее рассказываю о хуке useActionState и его применении.
Комментарии (4)

js2me
29.12.2024 08:13Подскажите пожалуйста, могу ошибаться, разве в вашем примере с useDeferredValue хуком при изменении состояния не произойдет ререндер как компонента, который содержит стейт поиска, так и дочерний компонент, куда передаётся deffered значение пропом?

andry36 Автор
29.12.2024 08:13Спасибо за коментарий, отличное наблюдение.
useDeferredValueпомогает распределять приоритеты рендеринга, чтобы сохранить отзывчивость интерфейса (например, при вводе текста). Однако, как подчёркивает и документация, для полного раскрытия потенциала этого хука тяжёлый компонент желательно обернуть вReact.memo. Без мемоизации он будет перерендериваться при каждом обновлении родителя, и все преимущество теряется. Поправлю пример в стате, чтобы сразу показать наглядное использование вместе сmemo.
Спасибо, что обратили на это внимание!
eshimischi
Спасибо за статью, а можно еще сделать разбор новых хуков, которые стали доступны с React 19 помимо useOptimistic?
andry36 Автор
Спасибо за отзыв!
Да, планирую сделать разбор и остальных новых хуков из React 19, чтобы охватить все интересные возможности (будет отдельная статья). А пока можете заглянуть в мою подробную статью о хуке useActionState: https://habr.com/ru/articles/870216/ - там тоже есть немало интересных идей по оптимизации и управлению состоянием. Спасибо, что читаете!