Пошук уроків, статей та іншого контенту
Використаємо useDeferredValue, щоб відкладати оновлення важких представлень під час введення даних.
useDeferredValueuseDeferredValue дозволяє відкласти оновлення частини інтерфейсу, яка не повинна блокувати взаємодію користувача.
Типовий сценарій:
Користувач вводить текст у поле.
Значення поля оновлюється негайно.
Важке представлення отримує відкладену версію цього значення.
React оновлює важке представлення тоді, коли має для цього достатньо ресурсів.
const deferredValue = useDeferredValue(value);На відміну від value, deferredValue може певний час містити попереднє значення.
Це особливо корисно для:
пошуку серед великого списку;
фільтрації складних даних;
оновлення графіків;
великих дерев компонентів;
попереднього перегляду документа;
важких результатів, які змінюються під час введення.
Розглянемо компонент пошуку. Список містить багато елементів, а результат пошуку рендерить лише частину з них.
import { memo, useDeferredValue, useMemo, useState } from "react";
const products = Array.from({ length: 15000 }, (_, index) => ({
id: index + 1,
name: `Product ${index + 1}`,
description: `Description for product ${index + 1}`,
}));
const SearchResults = memo(function SearchResults({ query }) {
const filteredProducts = useMemo(() => {
const normalizedQuery = query.trim().toLowerCase();
if (!normalizedQuery) {
return products;
}
return products.filter((product) => {
return (
product.name.toLowerCase().includes(normalizedQuery) ||
product.description.toLowerCase().includes(normalizedQuery)
);
});
}, [query]);
const visibleProducts = filteredProducts.slice(0, 300);
return (
<section>
<p>
Знайдено: {filteredProducts.length}
{filteredProducts.length > visibleProducts.length &&
`, показано: ${visibleProducts.length}`}
</p>
<ul>
{visibleProducts.map((product) => (
<li key={product.id}>
<strong>{product.name}</strong>
<div>{product.description}</div>
</li>
))}
</ul>
</section>
);
});
export default function App() {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const isResultsStale = query !== deferredQuery;
return (
<main>
<h1>Пошук товарів</h1>
<label>
Пошуковий запит
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Введіть назву товару"
/>
</label>
{isResultsStale && <p>Оновлення результатів…</p>}
<SearchResults query={deferredQuery} />
</main>
);
}Під час введення:
query оновлюється одразу;
поле введення залишається швидким;
deferredQuery може тимчасово містити попередній запит;
SearchResults працює зі значенням deferredQuery;
після завершення важкого оновлення результати відповідають актуальному запиту.
memoСам useDeferredValue не робить дочірній компонент автоматично дешевшим для повторного рендерингу.
У прикладі SearchResults обгорнутий у memo:
const SearchResults = memo(function SearchResults({ query }) {
// ...
});Коли батьківський компонент оновлюється через кожен символ, React може не перерендерювати SearchResults для кожного термінового оновлення, якщо його проп query ще не змінився.
Це важлива комбінація:
useDeferredValue дозволяє передати дочірньому компоненту старе значення.
memo дозволяє пропустити непотрібний рендер дочірнього компонента.
Важка робота виконується лише тоді, коли відкладене значення справді змінилося.
Без memo батьківський компонент усе одно може спричиняти рендер SearchResults, і перевага від відкладеного значення буде меншою.
useDeferredValue і debounce розв’язують різні задачі.
Debounce чекає певний проміжок часу перед виконанням операції:
користувач вводить → очікування 300 мс → запит або обчисленняЦе корисно, коли потрібно зменшити кількість:
HTTP-запитів;
викликів до зовнішнього сервісу;
дорогих операцій, які не мають виконуватися на кожен символ.
useDeferredValueuseDeferredValue не встановлює фіксовану затримку. Він повідомляє React, що певне оновлення має нижчий пріоритет.
користувач вводить → поле оновлюється → важке представлення оновлюється, коли можливоReact може оновити відкладене значення майже одразу або залишити попереднє значення довше, якщо основний потік зайнятий.
Отже:
для плавності інтерфейсу використовуйте useDeferredValue;
для обмеження кількості мережевих або зовнішніх операцій потрібен debounce;
useDeferredValue не є заміною debounce.
Поки важке представлення ще не оновилося, інтерфейс може показувати результати для попереднього запиту. Користувачу потрібно повідомити, що результати оновлюються.
Для цього можна порівняти актуальне і відкладене значення:
const isStale = query !== deferredQuery;Потім використати цей стан у розмітці:
{isStale && <p>Оновлення результатів…</p>}Або змінити прозорість старого представлення:
<section style={{ opacity: isStale ? 0.6 : 1 }}>
<SearchResults query={deferredQuery} />
</section>Важливо, що старий результат не обов’язково потрібно приховувати. Часто краще залишити його на екрані та додати індикатор оновлення. Так інтерфейс не буде миготіти й залишатиметься корисним під час обчислення.
useMemoУ компоненті результатів використано useMemo:
const filteredProducts = useMemo(() => {
return products.filter((product) => {
return product.name.includes(query);
});
}, [query]);Це дозволяє не виконувати фільтрацію повторно, якщо query не змінився.
У повному прикладі важке обчислення залежить саме від відкладеного пропу query. Тому воно запускається, коли React приймає нове відкладене значення.
useMemo не замінює useDeferredValue:
useMemo кешує результат обчислення;
useDeferredValue змінює пріоритет оновлення значення.
Для важкого списку часто потрібні обидва механізми.
useDeferredValue порівнює значення за ідентичністю. Для примітивів на кшталт рядків і чисел це зазвичай очікувана поведінка:
const deferredQuery = useDeferredValue(query);З об’єктами та масивами потрібно бути уважним:
const filters = {
category,
minPrice,
};
const deferredFilters = useDeferredValue(filters);Якщо об’єкт filters створюється під час кожного рендеру, він щоразу має нову ідентичність. Через це відкладене значення може вважатися зміненим навіть тоді, коли його дані фактично не змінилися.
Стабільніший варіант:
const filters = useMemo(
() => ({
category,
minPrice,
}),
[category, minPrice]
);
const deferredFilters = useDeferredValue(filters);Але мемоізувати об’єкти потрібно лише тоді, коли це справді впливає на продуктивність. Для одного рядка пошуку достатньо передавати примітивне значення.
useDeferredValueПід час швидкого введення deferredQuery може не відповідати query. Це очікувана поведінка, а не помилка.
const query = "react";
const deferredQuery = "rea";Компонент має бути готовим тимчасово показати результати для "rea".
React може перервати або перезапустити фоновий рендер, якщо з’явилося нове термінове оновлення. Тому компоненти мають залишатися чистими:
не змінюйте зовнішні змінні під час рендеру;
не виконуйте побічні ефекти в тілі компонента;
не покладайтеся на те, що кожен проміжний рендер буде завершено.
useDeferredValue не робить фільтрацію або рендеринг швидшими. Він допомагає не блокувати термінову взаємодію користувача важким оновленням.
Якщо саме обчислення надто дороге, додатково потрібно:
оптимізувати алгоритм;
зменшити кількість елементів, які рендеряться;
застосувати мемоізацію;
розділити великий компонент на менші частини.
useDeferredValue доречнийВикористовуйте його, коли:
є актуальне значення, яке має оновлюватися без затримки;
інша частина інтерфейсу залежить від цього значення;
ця частина може бути тимчасово неактуальною;
важке представлення не повинно блокувати поле введення або іншу взаємодію.
Прикладом є пошук у локальному наборі даних, де результати можна показати із невеликою затримкою.
Не варто робити так:
const deferredQuery = useDeferredValue(query);
<input
value={deferredQuery}
onChange={(event) => setQuery(event.target.value)}
/>У такому разі саме поле введення може відставати від дій користувача. Терміновий стан потрібно використовувати для контролю поля:
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
/>А deferredQuery передавати лише у важке представлення.
useDeferredValue не гарантує затримку в 100, 300 або 500 мілісекунд. Не використовуйте його як таймер.
memo для важкого дочірнього компонентаЯкщо важкий компонент не мемоізований, він може повторно рендеритися разом із батьківським компонентом. Перевірте, чи його пропси стабільні та чи доречний memo.
Відкладене значення призначене для відображення. Якщо на кожну зміну потрібно виконувати мережевий запит, useDeferredValue сам по собі не обмежить кількість таких запитів.
useDeferredValue створює версію значення з нижчим пріоритетом.
Термінова частина інтерфейсу, наприклад поле введення, оновлюється одразу.
Важке представлення може тимчасово використовувати старе значення.
Для користі від цього підходу важкий дочірній компонент часто потрібно поєднати з memo.
useMemo допомагає кешувати дороге обчислення, але не замінює useDeferredValue.
useDeferredValue не є debounce і не встановлює фіксовану затримку.
Порівняння актуального та відкладеного значення дозволяє показати індикатор оновлення.