useDeferredValue
useDeferredValue — это React Hook, который позволяет откладывать обновление части 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: Значение, которое вы хотите отложить. Оно может быть любого типа.- optional
initialValue: Значение, которое используется во время начального рендера компонента. Если этот параметр опущен,useDeferredValueне будет откладывать начальный рендер, потому что у него нет предыдущей версииvalue, которую можно было бы отрисовать вместо неё.
Возвращаемое значение
currentValue: Во время начального рендера возвращаемое отложенное значение будет равноinitialValueили тому же значению, которое вы передали. Во время обновлений React сначала попытается выполнить повторный рендер со старым значением (то есть вернёт старое значение), а затем попробует ещё один повторный рендер в фоне уже с новым значением (то есть вернёт обновлённое значение).
Предостережения
-
Когда обновление находится внутри Transition,
useDeferredValueвсегда возвращает новоеvalueи не запускает отложенный рендер, поскольку обновление уже отложено. -
Значения, которые вы передаёте в
useDeferredValue, должны быть либо примитивами (например, строками и числами), либо объектами, созданными вне рендера. Если вы создаёте новый объект во время рендера и сразу передаёте его вuseDeferredValue, он будет отличаться при каждом рендере, что вызовет лишние фоновые повторные рендеры. -
Когда
useDeferredValueполучает другое значение (по сравнению сObject.is), он не только сохраняет текущий рендер (где всё ещё использует предыдущий value), но и планирует повторный рендер в фоне с новым значением. Фоновый повторный рендер можно прервать: если придёт ещё одно обновлениеvalue, React перезапустит фоновый рендер с нуля. Например, если пользователь вводит текст в поле быстрее, чем график успевает перерисоваться с отложенным значением, график перерисуется только после того, как пользователь перестанет печатать. -
useDeferredValueинтегрирован с<Suspense>. Если фоновое обновление, вызванное новым значением, приостанавливает UI, пользователь не увидит fallback. Он будет видеть старое отложенное значение, пока данные не загрузятся. -
useDeferredValueсам по себе не предотвращает лишние сетевые запросы. -
У
useDeferredValueнет фиксированной задержки. Как только React завершает исходный повторный рендер, он сразу начинает работать над фоновым повторным рендером с новым отложенным значением. Любые обновления, вызванные событиями (например, вводом текста), прервут фоновый рендер и получат более высокий приоритет. -
Фоновый повторный рендер, вызванный
useDeferredValue, не запускает Effects, пока он не будет закоммичен на экран. Если фоновый рендер приостанавливается, его Effects будут запущены после загрузки данных и обновления UI.
Использование
Показывать устаревший контент, пока загружается свежий
Вызывайте useDeferredValue на верхнем уровне компонента, чтобы отложить обновление части UI.
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}Во время начального рендера отложенное значение будет таким же, как значение, которое вы передали.
Во время обновлений отложенное значение будет “отставать” от последнего значения. В частности, React сначала выполнит повторный рендер без обновления отложенного значения, а затем попробует перерендерить в фоне уже с только что полученным значением.
Давайте разберём пример, чтобы увидеть, когда это полезно.
В этом примере компонент SearchResults приостанавливается во время получения результатов поиска. Попробуйте ввести "a", дождаться результатов, а затем изменить текст на "ab". Результаты для "a" заменятся на fallback загрузки.
//App.js
import { Suspense, useState } from 'react';
import SearchResults from './SearchResults.js';
export default function App() {
const [query, setQuery] = useState('');
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={query} />
</Suspense>
</>
);
}Один из распространённых альтернативных UI-паттернов — отложить обновление списка результатов и продолжать показывать предыдущие результаты, пока не будут готовы новые. Вызовите useDeferredValue, чтобы передать вниз отложенную версию запроса:
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}query обновится сразу, поэтому input покажет новое значение. Однако deferredQuery будет удерживать предыдущее значение, пока данные не загрузятся, поэтому SearchResults некоторое время будет показывать устаревшие результаты.
Введите "a" в примере ниже, дождитесь загрузки результатов, а затем измените input на "ab". Обратите внимание: вместо fallback Suspense теперь показывается устаревший список результатов, пока не загрузятся новые:
//App.js
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>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}Как работает отложенное значение под капотом?
Можно представить это как два шага:
-
Сначала React повторно рендерит с новым
query("ab"), но со старымdeferredQuery(всё ещё"a"). ЗначениеdeferredQuery, которое вы передаёте списку результатов, отложено: оно “отстаёт” от значенияquery. -
В фоне React пытается повторно рендерить уже с
queryиdeferredQuery, обновлёнными до"ab". Если этот рендер завершится, React покажет его на экране. Однако если он приостановится (результаты для"ab"ещё не загрузились), React откажется от этой попытки и повторит рендер снова после загрузки данных. Пользователь продолжит видеть устаревшее отложенное значение, пока данные не будут готовы.
Фоновый отложенный рендер можно прервать. Например, если вы снова введёте текст в input, React бросит текущую попытку и начнёт заново уже с новым значением. React всегда будет использовать последнее переданное значение.
Обратите внимание: сетевой запрос всё равно будет выполняться на каждый символ. Здесь откладывается именно отображение результатов (до их готовности), а не сами сетевые запросы. Даже если пользователь продолжит печатать, ответы на каждый символ кэшируются, так что нажатие Backspace происходит мгновенно и не вызывает повторной загрузки.
Показывать, что контент устарел
В примере выше нет визуального указания, что список результатов для последнего запроса всё ещё загружается. Это может сбивать пользователя с толку, если новые результаты загружаются долго. Чтобы сделать это более очевидным, можно добавить визуальный индикатор, когда показывается устаревший список результатов:
<div style={{
opacity: query !== deferredQuery ? 0.5 : 1,
}}>
<SearchResults query={deferredQuery} />
</div>После такого изменения, как только вы начинаете печатать, устаревший список результатов станет немного бледнее, пока не загрузятся новые результаты. Можно также добавить CSS transition, чтобы затемнение появлялось плавно, как в примере ниже:
//App.js
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>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</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 медленно перерисовывается, её сложно оптимизировать, и вы хотите, чтобы она не блокировала остальной интерфейс.
Представьте, что у вас есть текстовое поле и компонент (например, график или длинный список), который перерисовывается на каждый символ:
function App() {
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={text} />
</>
);
}Сначала оптимизируйте SlowList, чтобы он пропускал повторный рендер, когда его props не меняются. Для этого оберните его в memo:
const SlowList = memo(function SlowList({ text }) {
// ...
});Однако это помогает только если props SlowList такие же, как и при предыдущем рендере. Проблема сейчас в том, что он медленный, когда props отличаются, а именно тогда вам и нужно показать другое визуальное состояние.
Конкретно, основная проблема производительности в том, что при каждом вводе символа в input SlowList получает новые props, и повторный рендер всего дерева делает ввод “дерганым”. В этом случае useDeferredValue позволяет отдать приоритет обновлению input (которое должно быть быстрым) над обновлением списка результатов (которое может быть более медленным):
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 понимает, что повторный рендер списка можно понизить в приоритете, чтобы он не блокировал ввод символов. Список будет “отставать” от input, а затем “догонять” его. Как и раньше, React постарается обновить список как можно скорее, но не будет мешать пользователю печатать.
Отложенный повторный рендер списка
В этом примере каждый элемент в компоненте SlowList искусственно замедлен, чтобы было видно, как useDeferredValue помогает сохранять отзывчивость input. Введите текст в input и обратите внимание, что печать остаётся плавной, а список “отстаёт”.
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} />
</>
);
}Неоптимизированный повторный рендер списка
В этом примере каждый элемент в компоненте SlowList искусственно замедлен, но здесь нет useDeferredValue.
Обратите внимание: ввод текста в input ощущается очень “дерганым”. Это происходит потому, что без useDeferredValue каждый символ заставляет весь список немедленно перерисоваться без возможности прерывания.
import { useState } from 'react';
import SlowList from './SlowList.js';
export default function App() {
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={text} />
</>
);
}Для этой оптимизации SlowList должен быть обёрнут в memo. Это нужно потому, что когда text меняется, React должен иметь возможность быстро перерисовать родительский компонент. Во время этого повторного рендера deferredText всё ещё хранит предыдущее значение, поэтому SlowList может пропустить повторный рендер (его props не изменились). Без memo ему всё равно пришлось бы перерисоваться, и смысл оптимизации был бы потерян.
Чем отложенное значение отличается от debouncing и throttling?
В этой ситуации вы могли раньше использовать две распространённые техники оптимизации:
- Debouncing означает, что вы ждёте, пока пользователь перестанет печатать (например, на секунду), прежде чем обновлять список.
- Throttling означает, что вы обновляете список время от времени (например, не чаще одного раза в секунду).
Хотя эти техники полезны в некоторых случаях, useDeferredValue лучше подходит для оптимизации рендера, потому что он глубоко интегрирован в сам React и адаптируется к устройству пользователя.
В отличие от debouncing и throttling, он не требует выбора фиксированной задержки. Если устройство пользователя быстрое (например, мощный ноутбук), отложенный повторный рендер произойдёт почти мгновенно и будет незаметен. Если устройство медленное, список будет “отставать” от input пропорционально тому, насколько медленно работает устройство.
Кроме того, в отличие от debouncing и throttling, отложенные повторные рендеры, выполненные с помощью useDeferredValue, по умолчанию можно прерывать. Это означает, что если React находится в процессе перерисовки большого списка, а пользователь вводит ещё один символ, React прервёт этот рендер, обработает ввод и затем снова начнёт фоновой рендер. Напротив, debouncing и throttling всё равно создают “дерганый” опыт, потому что они блокируют: они лишь откладывают момент, когда рендер начинает блокировать ввод.
Если оптимизируемая работа не происходит во время рендера, debouncing и throttling всё ещё полезны. Например, они могут позволить делать меньше сетевых запросов. Эти техники также можно использовать вместе.