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 + fetchuseEffect не работает с Suspense; нужны Suspense-совместимые библиотеки (React Query, SWR v2+, Relay).

Связанные темы

Ресурсы