Как работает useTransition и useDeferredValue в React?
useTransition и useDeferredValue в React
Оба хука появились в React 18 как часть модели Concurrent Rendering. Они позволяют React прерывать «тяжёлые» рендеры в пользу срочных обновлений (например, ввод пользователя), делая интерфейс отзывчивым.
useTransition
useTransition возвращает кортеж [isPending, startTransition]. Любое обновление состояния, обёрнутое в startTransition, React считает некритичным и может отложить или прервать, если появится более приоритетное обновление.
import { useState, useTransition } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<string[]>([]);
const [isPending, startTransition] = useTransition();
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
// Срочное обновление — поле ввода реагирует мгновенно
setQuery(e.target.value);
// Некритичное обновление — список результатов можно отложить
startTransition(() => {
setResults(heavyFilter(e.target.value));
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending ? <span>Загрузка...</span> : <ResultList items={results} />}
</>
);
}
isPending — флаг, который true пока отложенный рендер ещё не завершился. Это позволяет показывать индикатор загрузки без блокировки ввода.
useDeferredValue
useDeferredValue принимает значение и возвращает его «отложенную» копию. React рендерит компонент сначала со старым значением (мгновенно), а потом фоново пересчитывает с новым.
import { useState, useDeferredValue, memo } from 'react';
const HeavyList = memo(function HeavyList({ query }: { query: string }) {
// Дорогой рендер — тысячи элементов
const items = heavyFilter(query);
return <ul>{items.map(i => <li key={i}>{i}</li>)}</ul>;
});
function SearchPage() {
const [query, setQuery] = useState('');
// deferredQuery обновляется позже, чем query
const deferredQuery = useDeferredValue(query);
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
{/* Компонент получит старое значение, пока фон не пересчитает */}
<HeavyList query={deferredQuery} />
</>
);
}
Ключевые различия
| useTransition | useDeferredValue | |
|---|---|---|
| Управляет | вызовом setState | конкретным значением |
Есть isPending | да | нет (нужен сравнивать value с deferredValue) |
| Когда использовать | когда обновляете состояние сами | когда получаете значение из props/контекста |
Важные нюансы
- Оба хука работают только в Concurrent Mode (React 18+, без
ReactDOM.render). startTransitionне откладывает сетевые запросы — только рендер. Для серверных данных нуженuse+ Suspense или специальные хуки.useDeferredValueэффективен только вместе сmemo— иначе отложенный рендер всё равно пересчитывается синхронно.- Если машина пользователя мощная, разницы может не быть заметно — React адаптируется к скорости устройства.
- Нельзя читать
isPendingвнутриstartTransition— он становитсяtrueасинхронно после вызова.
Что хочет услышать интервьюер
Понимание модели Concurrent Rendering и того, что React может прерывать рендеры
Чёткое разграничение сценариев: useTransition — когда управляешь setState сам, useDeferredValue — когда значение приходит снаружи
Знание флага isPending и его практического применения для UX
Понимание, что useDeferredValue требует memo для реального эффекта
Осознание ограничений: хуки не откладывают сетевые запросы, работают только в Concurrent Mode
Пример: useTransition — откладываем тяжёлую фильтрацию при вводе
import { useState, useTransition } from 'react';
function heavyFilter(q: string): string[] {
// Имитация дорогой операции
return Array.from({ length: 10_000 }, (_, i) => `item-${i}`)
.filter(s => s.includes(q));
}
export function SearchWithTransition() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<string[]>([]);
const [isPending, startTransition] = useTransition();
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
// Ввод обновляется мгновенно
setQuery(e.target.value);
// Список обновляется с низким приоритетом
startTransition(() => {
setResults(heavyFilter(e.target.value));
});
}
return (
<div>
<input value={query} onChange={handleChange} placeholder="Поиск..." />
{isPending && <p>Обновление списка...</p>}
<ul>
{results.slice(0, 20).map(r => <li key={r}>{r}</li>)}
</ul>
</div>
);
}
Пример: useDeferredValue — откладываем props в дочернем компоненте
import { useState, useDeferredValue, memo } from 'react';
interface ListProps {
query: string;
}
// memo обязателен — без него отложенный рендер не даст эффекта
const HeavyList = memo(function HeavyList({ query }: ListProps) {
const items = Array.from({ length: 5_000 }, (_, i) => `result-${i}`)
.filter(s => s.includes(query));
return (
<ul>
{items.slice(0, 50).map(item => (
<li key={item}>{item}</li>
))}
</ul>
);
});
export function SearchWithDeferred() {
const [query, setQuery] = useState('');
// deferredQuery «отстаёт» от query при быстром вводе
const deferredQuery = useDeferredValue(query);
// Показываем, что список ещё не актуален
const isStale = query !== deferredQuery;
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Поиск..."
/>
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<HeavyList query={deferredQuery} />
</div>
</div>
);
}
Типичные ошибки
Считают, что startTransition откладывает fetch-запросы — он откладывает только рендер
Используют useDeferredValue без memo, не получают никакого эффекта
Путают useTransition с debounce/throttle — это разные механизмы с разной семантикой
Не проверяют isPending и не показывают пользователю индикатор ожидания, ухудшая UX
Пытаются использовать хуки в React 17 или в legacy-режиме ReactDOM.render


