← Назад к вопросам

Как работать с ассинхронными данными в React Suspense?

2.3 Middle🔥 231 комментариев
#React#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как работать с асинхронными данными в React Suspense

Suspense - это React компонент для обработки асинхронных операций (загрузка компонентов, данных). Он позволяет отложить рендеринг компонента, пока не будут загружены нужные данные, и показать fallback UI.

Основная концепция Suspense

import { Suspense } from 'react';

// Fallback UI которая показывается во время загрузки
function LoadingFallback() {
  return <div>Loading data...</div>;
}

function MyComponent() {
  return (
    <Suspense fallback={<LoadingFallback />}>
      <DataComponent />
    </Suspense>
  );
}

// DataComponent должен "throw promise" во время загрузки

Как работает Suspense изнутри

// Компонент может "throw" promise
// Это заставляет Suspense показать fallback

let cache = {};
let request = null;

function fetchData(url) {
  // Если данные уже в кеше - вернуть их
  if (url in cache) {
    return cache[url];
  }
  
  // Если promise уже создан - вернуть его (для одновременных запросов)
  if (request) {
    throw request;
  }
  
  // Создать новый promise и throw его
  request = fetch(url)
    .then(res => res.json())
    .then(data => {
      cache[url] = data;
      request = null;
      return data;
    })
    .catch(err => {
      request = null;
      throw err;
    });
  
  throw request;
}

function UserProfile() {
  // Это выполнится при каждом рендере
  // Во время загрузки выбросит promise
  const userData = fetchData('/api/user');
  
  return <div>{userData.name}</div>;
}

// Использование
function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <UserProfile />
    </Suspense>
  );
}

Правильный подход с use() хуком (React 19+)

import { use, Suspense } from 'react';

// Функция которая возвращает promise
function fetchUser(id) {
  return fetch(`/api/users/${id}`)
    .then(res => res.json());
}

function UserComponent({ userId }) {
  // use() распаковывает promise
  const user = use(fetchUser(userId));
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

function App() {
  return (
    <Suspense fallback={<div>Loading user...</div>}>
      <UserComponent userId={1} />
    </Suspense>
  );
}

Несколько Suspense компонентов для каскадной загрузки

import { Suspense, use } from 'react';

function Header() {
  const headerData = use(fetch('/api/header').then(r => r.json()));
  return <header>{headerData.title}</header>;
}

function Content() {
  const content = use(fetch('/api/content').then(r => r.json()));
  return <main>{content.body}</main>;
}

function Sidebar() {
  const sidebar = use(fetch('/api/sidebar').then(r => r.json()));
  return <aside>{sidebar.info}</aside>;
}

function App() {
  return (
    <div>
      {/* Header загружается и показывается отдельно */}
      <Suspense fallback={<div>Loading header...</div>}>
        <Header />
      </Suspense>
      
      <div style={{ display: 'flex' }}>
        {/* Content загружается независимо */}
        <Suspense fallback={<div>Loading content...</div>}>
          <Content />
        </Suspense>
        
        {/* Sidebar загружается независимо */}
        <Suspense fallback={<div>Loading sidebar...</div>}>
          <Sidebar />
        </Suspense>
      </div>
    </div>
  );
}

Использование с Server Components (Next.js)

// app/user/[id]/page.tsx
import { Suspense } from 'react';

async function UserData({ userId }) {
  // Это Server Component - можно использовать async
  const res = await fetch(`/api/users/${userId}`, {
    next: { revalidate: 60 }
  });
  const user = await res.json();
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

async function UserComments({ userId }) {
  const res = await fetch(`/api/users/${userId}/comments`, {
    next: { revalidate: 30 }
  });
  const comments = await res.json();
  
  return (
    <div>
      {comments.map(comment => (
        <div key={comment.id}>{comment.text}</div>
      ))}
    </div>
  );
}

export default function Page({ params }) {
  return (
    <div>
      <Suspense fallback={<div>Loading user...</div>}>
        <UserData userId={params.id} />
      </Suspense>
      
      <Suspense fallback={<div>Loading comments...</div>}>
        <UserComments userId={params.id} />
      </Suspense>
    </div>
  );
}

Обработка ошибок с Error Boundary

import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

function DataComponent() {
  const data = use(fetchDataThatCanFail());
  return <div>{data}</div>;
}

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <Suspense fallback={<div>Loading...</div>}>
        <DataComponent />
      </Suspense>
    </ErrorBoundary>
  );
}

Паттерн с кешированием запросов

class QueryCache {
  constructor() {
    this.cache = new Map();
  }
  
  get(key, fn) {
    if (this.cache.has(key)) {
      return this.cache.get(key);
    }
    
    const promise = fn().catch(err => {
      this.cache.delete(key);
      throw err;
    });
    
    this.cache.set(key, promise);
    return promise;
  }
  
  invalidate(key) {
    this.cache.delete(key);
  }
}

const cache = new QueryCache();

function useQuery(key, fn) {
  return use(cache.get(key, fn));
}

function Users() {
  const users = useQuery('users', () => 
    fetch('/api/users').then(r => r.json())
  );
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

function App() {
  return (
    <Suspense fallback={<div>Loading users...</div>}>
      <Users />
    </Suspense>
  );
}

Lazy loading компонентов

import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));
const AnotherComponent = lazy(() => import('./AnotherComponent'));

function App() {
  const [showHeavy, setShowHeavy] = React.useState(false);
  
  return (
    <div>
      <button onClick={() => setShowHeavy(!showHeavy)}>
        Toggle Heavy Component
      </button>
      
      {showHeavy && (
        <Suspense fallback={<div>Loading component...</div>}>
          <HeavyComponent />
        </Suspense>
      )}
      
      <Suspense fallback={<div>Loading...</div>}>
        <AnotherComponent />
      </Suspense>
    </div>
  );
}

Ключевые моменты

  1. Suspense ловит thrown promises от компонентов
  2. use() хук распаковывает promise в синхронные данные
  3. Несколько Suspense компонентов загружаются параллельно
  4. Fallback показывается пока promise не разрешится
  5. Можно сочетать с Error Boundary для обработки ошибок
  6. Server Components в Next.js работают отлично с Suspense
  7. Lazy loading компонентов работает через Suspense
  8. Кеширование запросов важно для оптимизации

Suspense - это мощный механизм для управления асинхронностью в React, который делает код более читаемым и управляемым.