React

useDeferredValue

Тяжёлый компонент - визуализация графа на тысячи узлов - перерисовывается на каждое значение из поля поиска. Поле и граф связаны напрямую, и при наборе каждая буква запускает дорогую перерисовку графа, отчего ввод дёргается. useTransition тут неудобен: обновление поля приходит из встроенного события input, его не обернёшь в startTransition без ломки. Нужен другой угол - не откладывать обновление, а отложить само значение, которое уходит в тяжёлый потомок. Ровно это делает useDeferredValue: поле получает свежее значение мгновенно, а граф - слегка отстающую версию, рендерясь в фоне с низким приоритетом.

  • Поиск с тяжёлой выдачей: поле обновляется свежим значением, а дорогой список рисуется по отложенному
  • Визуализации данных: графы, диаграммы, тепловые карты, где перерасчёт на каждый символ ощутимо тормозит
  • Подсветка синтаксиса в больших редакторах: текст печатается плавно, подсветка догоняет с низким приоритетом
  • Фильтры над большими таблицами, когда значение фильтра приходит как пропс и его неудобно оборачивать в транзицию
  • React 18+ (2022): useDeferredValue - штатный конкурентный хук рядом с useTransition

Предварительные знания

  • Модель срочных и несрочных обновлений из урока про идею конкурентности
  • useTransition: понимание, что несрочную работу React может прервать и отложить
  • Мемоизация: memo и useMemo, чтобы дорогой потомок не пересчитывался зря

Второй инструмент той же модели

useDeferredValue вышел стабильно вместе с useTransition в React 18 (март 2022) как вторая грань одной конкурентной модели. Команда осознавала, что не всегда у разработчика под рукой само обновление состояния: значение часто приходит сверху как пропс или из источника, который не обернуть в startTransition. Для таких случаев и придумали хук, который принимает значение и возвращает его отложенную версию. Идея та же - дать срочной части интерфейса свежие данные, а тяжёлой производной части позволить отставать и рендериться с низким приоритетом, не блокируя ввод. К 2026 году оба хука сосуществуют, покрывая разные точки приложения одного и того же принципа.

Как работает отложенное значение

useDeferredValue принимает значение и возвращает его копию, которая 'отстаёт' при быстрых изменениях. Когда исходное значение меняется, хук сначала возвращает прежнее отложенное значение, а в фоне с низким приоритетом запускает рендер с новым. Если за это время значение снова изменилось, незавершённый фоновый рендер отбрасывается - ровно как в модели конкурентности. В итоге свежее значение доступно срочной части сразу, а тяжёлая часть догоняет.

Критически важная деталь: сам по себе хук не ускоряет потомка. Чтобы появился выигрыш, тяжёлый потомок должен быть обёрнут в memo. Тогда при наборе текста query меняется быстро, а deferredQuery остаётся прежним до завершения фонового рендера - и memo пропускает повторный рендер графа, пока отложенное значение не изменилось. Без memo граф пересчитывался бы всё равно, и смысл откладывания пропал бы.

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

Почему useDeferredValue без мемоизации тяжёлого потомка не даёт выигрыша?

Разница с useTransition

Оба хука опираются на одну модель приоритетов и дают похожий результат - отзывчивый ввод при тяжёлом рендере. Различие в том, за что они цепляются. useTransition оборачивает обновление состояния: разработчик сам вызывает setState внутри startTransition. useDeferredValue работает со значением: он не управляет тем, как значение появилось, а лишь отдаёт его отстающую версию вниз по дереву.

  • useTransition — Цепляется за обновление состояния. Нужен, когда разработчик сам вызывает setState и может обернуть его в startTransition. Даёт флаг isPending.
  • useDeferredValue — Цепляется за значение. Нужен, когда значение приходит извне (пропс, чужое состояние) и обновление не под контролем. Возвращает отложенную копию.

Практическое правило выбора простое. Если у разработчика под рукой само обновление состояния - подходит useTransition. Если же тяжёлый потомок зависит от значения, которое приходит сверху как пропс и не оборачивается в setState - подходит useDeferredValue. Часто это вопрос того, на каком уровне дерева находится код: владелец состояния берёт транзицию, получатель пропса - отложенное значение.

Признак отставания у useDeferredValue можно получить вручную: сравнить query !== deferredQuery. Если они различаются, значит идёт фоновый рендер по новому значению - удобно, чтобы приглушить устаревший контент, как isPending у транзиции. У самого хука отдельного флага нет, сравнение делают сами.

В каком случае useDeferredValue удобнее, чем useTransition?

Выбор и подводные камни

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

  1. Найти тяжёлый потомок, который перерисовывается при каждом изменении значения
  2. Обернуть этот потомок в memo, а его дорогое вычисление в useMemo
  3. Получить отложенную версию значения через useDeferredValue и передать её именно в потомок
  4. Срочной части (полю ввода) оставить свежее, не отложенное значение
  5. При желании сравнить свежее и отложенное значение, чтобы приглушить устаревший контент

Главная ошибка - передать отложенное значение в само поле ввода. Тогда текст в поле начнёт отставать, ведь поле будет показывать устаревшую версию. Отложенное значение уходит только в тяжёлого потомка, а срочная часть всегда работает со свежим значением. Это зеркало ошибки из урока про useTransition.

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

С React Compiler v1.0 (2025) ручная мемоизация потомка часто появляется сама, и тогда useDeferredValue работает, не требуя руками расставленных memo и useMemo. Но условие остаётся прежним: чтобы отложенное значение давало выигрыш, тяжёлый потомок должен уметь пропускать рендер при неизменном значении - неважно, кто эту мемоизацию обеспечил.

Что произойдёт, если передать отложенное значение прямо в поле ввода?

Связь с другими темами

useDeferredValue - парный к useTransition инструмент той же модели:

  • useTransition — Решает ту же задачу, но через обёртку обновления состояния, а не через отложенное значение
  • Идея конкурентности — Приоритезация срочное/несрочное, которую useDeferredValue применяет к значению
  • Мемоизация — Обязательное условие выигрыша: тяжёлый потомок должен пропускать рендер при неизменном отложенном значении

Итог

  • useDeferredValue принимает значение и возвращает его отложенную версию, отстающую при быстрых изменениях
  • Срочная часть (поле ввода) использует свежее значение, а тяжёлый потомок - отложенное и рендерится с низким приоритетом
  • Главное отличие от useTransition: откладывается значение, а не обновление состояния, поэтому хук удобен для пропсов извне
  • Выигрыш проявляется только если тяжёлый потомок мемоизирован и пропускает рендер при неизменном отложенном значении
  • Как и транзиция, это не ускорение рендера, а перераспределение приоритета: дорогой рендер отстаёт, но не блокирует ввод

Связанные уроки

  • rc-24-concurrent-intro — useDeferredValue реализует приоритезацию из модели конкурентности, откладывая значение с низким приоритетом
  • rc-25-usetransition — Тот же эффект отзывчивости, но useTransition оборачивает обновление состояния, а useDeferredValue - производное значение
  • rc-19-memoization — Без мемоизации дорогого потомка отложенное значение не даст выигрыша - потомок всё равно будет пересчитываться
useDeferredValue

0

1

Войти