useDeferredValue даёт копию значения, которой разрешено отставать. Когда значение меняется, React сначала перерисовывает экран со старой копией, что сохраняет ввод мгновенным, а затем в фоне рендерит медленную часть с новым значением.
Быстро наберите слово. Поле успевает за вводом, а список тускнеет, пока отстаёт. Console выводит по строке SlowList rendering на каждую букву, потому что SlowList выводит сообщение, как только React начинает его рендерить, но списки для промежуточных значений так и не доходят до экрана: каждая новая буква прерывала фоновый рендер, и React начинал заново с последним текстом. Замените query={deferredQuery} на query={query} и снова попечатайте: теперь каждое нажатие клавиши ждёт список четверть секунды.
Синтаксис
const deferredValue = useDeferredValue(value);
const deferredValue = useDeferredValue(value, initialValue); // React 19
valueэто всё, что вы рендерите: строка из поля, число, объект. Лучше всего работают примитивы, потому что новый объект при каждом рендере всегда считается изменением.- Возвращаемое значение большую часть времени равно
value. Сразу после измененияvalueоно на один рендер хранит предыдущее значение и догоняет в фоновом рендере.
Как это работает
Каждое изменение даёт два рендера:
- Срочный рендер с новым
valueи старымdeferredValue. Поле показывает новый текст. Компоненты, которые получают толькоdeferredValue, получают те же пропсы, что и раньше. - Фоновый рендер с новым
deferredValue. Это медленный рендер. React рендерит его частями и бросает, еслиvalueснова изменится до его завершения, а затем начинает заново с последним значением.
Это можно увидеть, выводя оба значения:
Наберите одну букву, скажем a, и Console выведет ещё две строки: сначала text = "a" с deferred = "", затем оба "a". Именно первый рендер сохраняет ввод быстрым, но только если медленная часть может его пропустить. Поэтому SlowList в первом примере обёрнут в memo: во время срочного рендера его пропс query не изменился, и React его пропускает. Без memo срочный рендер всё равно выполнял бы весь медленный список, только со старым текстом, и вы ничего бы не выиграли.
Показ устаревшего содержимого
Пока идёт фоновый рендер, value и отложенное значение различаются. Сравните их, чтобы сообщить пользователю, что содержимое устарело, не скрывая его:
const isStale = query !== deferredQuery;
<div style={{ opacity: isStale ? 0.5 : 1, transition: 'opacity 0.2s' }}>
<SlowList query={deferredQuery} />
</div>
Обычно это приятнее спиннера: старые результаты остаются читаемыми и меняются на месте, когда новые готовы. Та же идея работает с Suspense: если отложенное значение заставляет компонент приостановиться при загрузке данных, React продолжает показывать старое содержимое вместо запасного варианта, пока новое не будет готово (см. Suspense и lazy).
Начальное значение (React 19)
По умолчанию первому рендеру не от чего отставать, поэтому отложенное значение с самого начала равно настоящему. Начиная с React 19 можно передать второй аргумент. При первом рендере хук возвращает initialValue, а затем React снова рендерит в фоне с настоящим значением. Это позволяет сразу показать что-то дешёвое и сразу после заполнить дорогую часть.
Console показывает два рендера: сначала с "", затем с полным текстом. Уберите второй аргумент, и выведется только один рендер, сразу с полным текстом.
useDeferredValue или useTransition
Оба помечают работу как несрочную и используют один и тот же механизм рендеринга. Разница в том, где их ставить:
- useTransition оборачивает обновление. Вы вызываете
startTransition(() => setQuery(text)), поэтому нужен сеттер.isPendingвы получаете бесплатно. - useDeferredValue оборачивает значение. Вы вызываете его там, где значение используется, поэтому он работает, когда значение приходит как пропс или из хука, который вы не контролируете. «Ожидание» вы вычисляете сами через
value !== deferredValue.
// You own the state: either works, a transition is direct
startTransition(() => setQuery(nextQuery));
// You only receive the value
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
return <SlowList query={deferredQuery} />;
}
useDeferredValue, debounce и throttle
Debounce ждёт фиксированное время после последнего изменения; throttle обновляет не чаще одного раза за интервал. Оба заранее выбирают задержку, поэтому на быстром ноутбуке делают страницу медленнее, чем нужно, а на медленном телефоне задержки всё равно может не хватить.
У useDeferredValue задержки нет. Фоновый рендер начинается сразу, поэтому на быстром устройстве список обновляется почти мгновенно, а на медленном React сохраняет отзывчивость поля и рендерит список так часто, как может. К тому же фоновую работу можно прервать, а рендер после debounce нельзя: начавшись, он блокирует, пока не закончится.
Чего он не делает, так это не сокращает сетевые запросы. Если каждое новое значение запускает fetch, отложенное значение всё равно меняется с каждым завершённым рендером, поэтому применяйте debounce к самому запросу, когда важен объём запросов.
Частые ошибки
- Забыть
memo. Как показано выше, медленный компонент должен иметь возможность пропустить срочный рендер. Если он не обёрнут вmemoили один из его других пропсов при каждом рендере новый объект, он рендерится дважды за нажатие клавиши вместо одного раза. - Передать новый объект.
useDeferredValue({ query })при каждом рендере создаёт новый объект, поэтому отложенное значение всегда выглядит изменившимся, и React каждый раз запускает лишний фоновый рендер. Откладывайте примитив (useDeferredValue(query)) и собирайте объекты из него. - Ожидать меньше запросов. Компонент, который загружает данные по отложенному значению, всё равно загружает их для каждого значения, рендер которого завершился.
Когда за него браться
Используйте useDeferredValue, когда одно поле управляет частью страницы, которая медленно рендерится, медленную часть можно мемоизировать, и вас устраивает на мгновение показывать немного устаревшие результаты: результаты поиска, график, который перерисовывается по фильтру, превью форматированного текста. Если медленная часть медленная по причине, которую можно исправить, например рендер 5000 строк, когда видно 50, сначала исправьте это. Вариант этого шаблона, когда сеттер принадлежит вам, описан на странице о useTransition.
Часто задаваемые вопросы
Что делает useDeferredValue?
Он возвращает версию значения, которая может отставать от настоящего. Когда значение меняется, React сначала рендерит со старым отложенным значением, а затем снова в фоне с новым, и этот фоновый рендер можно прервать.
Почему useDeferredValue не ускоряет мой список?
Медленный компонент должен быть обёрнут в memo и получать отложенное значение. Иначе срочный рендер всё равно рендерит медленный компонент, просто со старым значением, и ввод остаётся медленным.
Чем useDeferredValue отличается от useTransition?
useTransition оборачивает обновление состояния, поэтому нужен доступ к сеттеру. useDeferredValue оборачивает значение, поэтому работает, когда вы получаете только значение, например как пропс или из хука, который вы не контролируете.
useDeferredValue это debounce?
Нет. Фиксированной задержки нет. React начинает отложенный рендер сразу после срочного, и на быстром устройстве отставание едва заметно. Сам по себе он также не уменьшает число сетевых запросов.
Что за второй аргумент у useDeferredValue?
Начиная с React 19 можно передать initialValue. При первом рендере хук возвращает его, а затем React снова рендерит в фоне с настоящим значением.