
Веб-приложения сегодня требуют всё большей интерактивности, отзывчивости и быстродействия. В ответ на это команда 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)
 - js2me29.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/ - там тоже есть немало интересных идей по оптимизации и управлению состоянием. Спасибо, что читаете!