Suspense и Concurrent Features
React Suspense — механизм декларативного отображения fallback UI пока компонент «ждёт» чего-то (загрузку кода, данных); Concurrent Features — набор возможностей React 18+, позволяющих прерывать и приоритизировать рендеринг.
Зачем нужно
До Concurrent Mode React рендерил дерево синхронно: начав рендер, нельзя было его прервать — интерфейс «замораживался» на тяжёлых операциях. Concurrent Features делают рендеринг прерываемым: React может начать рендерить, обнаружить, что пользователь нажал кнопку, бросить текущий рендер и обработать взаимодействие. Suspense декларативно управляет состоянием загрузки без ручного isLoading.
Где используется
- Lazy loading маршрутов и компонентов (Suspense +
React.lazy) - Загрузка данных с Suspense-совместимыми библиотеками (React Query, SWR, Relay)
useTransitionдля плавных фильтраций больших списковuseDeferredValueдля отложенного обновления поиска без блокировки ввода
Suspense для lazy loading
import { Suspense, lazy } from 'react';
const HeavyChart = lazy( => import('./HeavyChart'));
function Dashboard() {
return (
<div>
<h1>Дашборд</h1>
{/* fallback показывается пока HeavyChart.js загружается */}
<Suspense fallback={<div className="skeleton">Загрузка графика...</div>}>
<HeavyChart />
</Suspense>
</div>
);
}
useTransition — некритические обновления
import { useState, useTransition } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState();
const [isPending, startTransition] = useTransition;
const handleSearch = (e) => {
// Критическое: обновить input мгновенно
setQuery(e.target.value);
// Некритическое: можно отложить (помечаем как transition)
startTransition(() => {
// React может прервать это обновление если придёт более важное
const found = searchItems(e.target.value);
setResults(found);
});
};
return (
<div>
<input value={query} onChange={handleSearch} />
{/* isPending: true пока transition не завершён */}
{isPending && <p>Поиск...</p>}
<ResultsList items={results} />
</div>
);
}
useDeferredValue
import { useState, useDeferredValue } from 'react';
function FilteredList({ items }) {
const [filter, setFilter] = useState('');
// Отложенное значение — обновляется после критических обновлений
const deferredFilter = useDeferredValue(filter);
// Этот рендер использует отложенное значение
const filtered = items.filter(item =>
item.name.includes(deferredFilter)
);
return (
<div>
{/* input обновляется мгновенно */}
<input value={filter} onChange={e => setFilter(e.target.value)} />
{/* список рендерится с задержкой, не блокируя ввод */}
<ul>
{filtered.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
Suspense для данных (React Query)
import { Suspense } from 'react';
import { useSuspenseQuery } from '@tanstack/react-query';
// Компонент может «бросить» промис — Suspense его поймает
function UserProfile({ userId }) {
// useSuspenseQuery «бросает» промис пока данные не готовы
const { data: user } = useSuspenseQuery({
queryKey: ['user', userId],
queryFn: => fetchUser(userId),
});
return <div>{user.name}</div>;
}
function App() {
return (
<Suspense fallback={<p>Загрузка профиля...</p>}>
<UserProfile userId="42" />
</Suspense>
);
}
Частые ошибки
- Suspense без Error Boundary — если промис rejected, нужен Error Boundary рядом с Suspense.
- useTransition для критических обновлений — ввод пользователя, клики должны обновляться мгновенно; не помечайте их как transition.
- Suspense с обычным
useEffect+ fetch —useEffectне работает с Suspense; нужны Suspense-совместимые библиотеки (React Query, SWR v2+, Relay).
Связанные темы
- _MOC SPA
- Lazy Loading маршрутов
- Error Boundaries
- Загрузка данных и loading states
- React Server Components