useDeferredValue
Хук useDeferredValue
позволяет откладывать обновление для части UI.
const deferredValue = useDeferredValue(value)
Справочник
useDeferredValue(value, initialValue?)
Чтобы сделать обновления значения отложенными, вызовите useDeferredValue
с этим значением на верхнем уровне своего компонента:
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}
Параметры
value
: Значение, обновление которого вы хотите отложить.- Canary only необязательный
initialValue
: Значение, установленное для первого рендера. Если этот параметр опущен,useDeferredValue
не сработает для первого рендера, так как нет предыдущей версииvalue
, которую можно было бы показать.
Возвращаемое значение
currentValue
: При первом рендеринге вызов вернёт то же значение, которое вы указали. Когда в следующих обновлениях значение изменится, вызов вернёт прошлое значение, но при этом React запустит дополнительный фоновый рендеринг, в котором вызов вернёт обновлённое значение.
Замечания
-
Когда обновление происходит внутри перехода (Transition), оно уже отложено, поэтому
useDeferredValue
всегда возвращает новое значениеvalue
и не приводит к повторному рендеру. -
Значения, которые вы передаёте в
useDeferredValue
, должны либо быть примитивного типа (как, например, строки или числа), либо должны создаваться не во время рендеринга. Если вы будете во время рендеринга каждый раз передавать вuseDeferredValue
свеже созданный объект, то так вы будете постоянно запускать ненужный фоновый рендеринг. -
Когда
useDeferredValue
получит другое значение (сравниваться будет черезObject.is
), помимо текущего рендеринга (в котором хук вернёт старое значение), дополнительно в фоне запустится рендеринг для собственно нового значения. Но этот фоновый рендеринг может прерваться: если значение параметраvalue
изменится ещё раз, то React перезапустит фоновый рендеринг заново. Например, если пользователь будет печатать быстрее, чем зависящий от ввода график будет успевать в фоне рендерить предыдущий ввод — график в таком случае обновится, только когда пользователь перестанет печатать. -
useDeferredValue
интегрирован с<Suspense>
. Если фоновое обновление для нового значения задержится, то вместо заглушки<Suspense>
пользователь просто увидит старое значение, пока загружаются данные для фонового обновления. -
Сам по себе
useDeferredValue
не защищает от лишних запросов в сеть. -
useDeferredValue
не пытается отложить обновление на какое-то конкретное количество времени. Как только React закончит с текущим рендерингом, он сразу же запустит в фоне рендеринг для новой версии отложенного значения. А любые обновления из-за внешних событий (пользователь печатает, например), будут просто более приоритетными, чем фоновый рендеринг, и прервут его. -
Эффекты фонового рендеринга, вызванного
useDeferredValue
, сработают, только когда React зафиксирует результат на экране. Если фоновый рендеринг запросит задержку, то эффекты сработают только после того, как данные загрузятся, а экран обновится.
Применение
Отображение старых данных, пока загружаются новые
Чтобы отложить обновление для части UI, вызовите useDeferredValue
на верхнем уровне своего компонента:
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}
При первом рендеринге отложенное значение будет равно значению, которое вы передадите.
В следующих обновлениях отложенное значение будет как бы “отставать” от актуального значения. А именно: сначала React отрендерит компонент, не обновляя отложенное значение, а затем в фоне попытается отрендерить компонент с новым значением.
Разберём на примере, когда это может быть полезно.
В этом примере компонент SearchResults
задерживается, т.к. отправляет поисковый запрос. Попробуйте ввести "a"
, дождаться результатов поиска, и затем ввести "ab"
. На месте результатов по запросу "a"
ненадолго появится индикатор загрузки.
import { Suspense, useState } from 'react'; import SearchResults from './SearchResults.js'; export default function App() { const [query, setQuery] = useState(''); return ( <> <label> Найти альбом: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Загрузка...</h2>}> <SearchResults query={query} /> </Suspense> </> ); }
Однако здесь можно применить другой частый паттерн в UI: отложить обновление списка результатов, продолжив показывать старые результаты, пока не подготовятся новые. Чтобы показать результаты поиска по отложенной версии запроса, можно применить useDeferredValue
:
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Найти альбом:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Загрузка...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}
Значение query
будет всегда актуальным — соответственно и отображаемый в поле ввода запрос. Но в deferredQuery
будет предыдущее значение запроса, пока не загрузятся новые результаты поиска — поэтому SearchResults
ещё некоторое время будет показывать старые результаты.
В изменённом примере ниже введите "a"
, дождитесь загрузки результатов поиска, и затем измените запрос на "ab"
. Обратите внимание, что теперь, пока загружаются новые результаты, вместо индикатора загрузки (заглушки Suspense) отображаются предыдущие результаты.
import { Suspense, useState, useDeferredValue } from 'react'; import SearchResults from './SearchResults.js'; export default function App() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); return ( <> <label> Найти альбом: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Загрузка...</h2>}> <SearchResults query={deferredQuery} /> </Suspense> </> ); }
Deep Dive
Для простоты удобно представлять, что обновление происходит в два этапа:
-
Сначала React отрендерит компонент с новым запросом
"ab"
вquery
, но пока что с отложенным"a"
вdeferredQuery
. Значение вdeferredQuery
, которое вы передаёте в список результатов, является отложенным: оно “отстаёт” от значенияquery
. -
Затем в фоне React попытается ещё раз отрендерить компонент, но уже с новым запросом
"ab"
и вquery
, и вdeferredQuery
. Если этот рендеринг выполнится до конца, то React отобразит его результаты на экране. Но если рендеринг задержится (встанет в ожидании результатов для"ab"
), то React эту конкретную попытку прервёт, а когда результаты загрузятся, попробует снова. Пока данные не загрузились, пользователю будет показываться старое отложенное значение.
Отложенный фоновый рендеринг можно прервать. Если, например, продолжить печатать запрос, React прервёт фоновый рендеринг и перезапустит его уже с новым вводом. React всегда будет ориентироваться только на самое последнее переданное ему значение.
В этом примере важно обратить внимание, что запросы в сеть всё ещё отправляются по каждому нажатию на клавиатуре. Откладывается здесь именно обновление результатов на экране, а не отправка в сеть запроса поиска. Просто запрос по каждому нажатию кэшируется — поэтому по удалению символа результат уже без запроса мгновенно берётся из кэша.
Подсветка неактуальных данных
В предыдущем примере в списке последних результатов никак не обозначалось, что результаты по новому запросу всё ещё загружаются. Такой интерфейс может сбить с толку, особенно если новые результаты будут загружаться долго. Решить проблему можно, добавив визуальную индикацию для случая, когда отображаемый список результатов больше не актуален и не соответствует последнему запросу:
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1,
}}>
<SearchResults query={deferredQuery} />
</div>
Благодаря этим изменениям, когда вы начнёте набирать новый запрос, список старых результатов потускнеет, пока не загрузится новый список. Вы даже можете добавить анимированный переход с задержкой, чтобы визуально “устаревание” ощущалось постепенным. Например:
import { Suspense, useState, useDeferredValue } from 'react'; import SearchResults from './SearchResults.js'; export default function App() { const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); const isStale = query !== deferredQuery; return ( <> <label> Найти альбом: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Загрузка...</h2>}> <div style={{ opacity: isStale ? 0.5 : 1, transition: isStale ? 'opacity 0.2s 0.2s linear' : 'opacity 0s 0s linear' }}> <SearchResults query={deferredQuery} /> </div> </Suspense> </> ); }
Откладывание повторного рендеринга для части UI
useDeferredValue
— это в том числе инструмент оптимизации. Его можно применить в ситуации, когда какая-то часть вашего UI требует вычислительно долгого рендеринга, с которым очень трудно что-то сделать, но при этом вы не хотите из-за этого постоянно блокировать рендеринг остального UI.
Представьте, что в вашем приложении некий сложный компонент (график, либо очень длинный список) каждый раз заново рендерится по каждому нажатию клавиши в поле ввода:
function App() {
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={text} />
</>
);
}
Для начала, вы можете обернуть SlowList
в memo
, чтобы рендеринг SlowList
не повторялся, если его пропсы не меняются.
const SlowList = memo(function SlowList({ text }) {
// ...
});
Но этого не достаточно. Ведь так рендеринг ускорится, только если всегда передавать в SlowList
одни и те же значения пропсов. Проблема в том, что рендеринг всё ещё медленный, если передавать другие значения пропсов, требующие другой визуализации.
Конкретно в этом примере, SlowList
будет на каждый ввод символа получать новые пропсы и своим рендерингом блокировать остальной интерфейс. Из-за чего ввод будет слишком заметно “заедать”. В такой ситуации с помощью useDeferredValue
можно сделать обновления поля ввода всегда более приоритетными (отзывчивее), чем обновления списка (которые в любом случае медленные):
function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={deferredText} />
</>
);
}
Собственно сам рендеринг SlowList
не станет от этого быстрее. Однако теперь React понимает, что не нужно блокировать обработку нажатий рендерингом списка. Визуально список будет как бы “отставать” от ввода, а затем его “догонять”. Конечно, как и до оптимизации, React будет стараться обновлять список как можно раньше, но уже не в ущерб возможности печатать.
Example 1 of 2: Отложенный рендеринг списка
В этом примере каждый элемент в компоненте SlowList
искусственно замедлен, чтобы продемонстрировать, как useDeferredValue
позволяет сохранить отзывчивость поля ввода. Попробуйте попечатать в поле ввода — оцените свои ощущения от того, как мгновенно оно реагирует на ввод, хотя список при этом заметно отстаёт.
import { useState, useDeferredValue } from 'react'; import SlowList from './SlowList.js'; export default function App() { const [text, setText] = useState(''); const deferredText = useDeferredValue(text); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <SlowList text={deferredText} /> </> ); }
Deep Dive
Возможно, вы в похожей ситуации применили бы один из двух распространённых приёмов:
- Дебаунсинг (debouncing), при котором приложение сначала бы дожидалось, когда пользователь перестанет печатать (уже секунду не печатал, например), и потом обновляло список.
- Тротлинг (throttling), при котором, как бы быстро пользователь ни печатал, приложение обновляло бы список не чаще одного раза за какой-то период (раз в секунду, например).
Хотя эти методы полезны в некоторых случаях, useDeferredValue
лучше подходит для оптимизации рендеринга, поскольку он тесно взаимодействует с React и может подстроиться под возможности устройства пользователя.
Можно не привязываться к какой-то фиксированной задержке. У пользователей с быстрым, мощным устройством фоновый рендеринг будет выполняться быстро и без заметной задержки. А у пользователей со слабым устройством список будет “отставать” ровно на столько, на сколько позволяет устройство.
Кроме того, в отличие от дебаунсинга и тротлинга, отложенный с помощью useDeferredValue
рендеринг можно прервать. Это значит, что если, например, пользователь введёт очередной символ, пока в фоне рендерится большой сложный список, React прервёт этот рендеринг, обработает ввод, и затем снова запустит рендеринг в фоне. При этом с дебаунсингом или тротлингом в такой же ситуации интерфейс всё ещё будет тормозить и заедать — ведь эти приёмы не устраняют собственно блокировку ввода: с ними она случается просто либо позже, либо реже.
Когда нужно оптимизировать что-то помимо рендеринга, дебаунсинг и тротлинг могут наоборот быть очень полезны. Например, они помогут уменьшить количество запросов в сеть. А ещё их можно совмещать с описанными здесь техниками.