Веб-приложения сегодня требуют всё большей интерактивности, отзывчивости и быстродействия. В ответ на это команда 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
и его применении.
ссылка на оригинал статьи https://habr.com/ru/articles/870748/
Добавить комментарий