Асинхронные компоненты в React: новый стандарт работы с данными

16 марта 2026
Автор

Олег Марков

Асинхронные компоненты в React

Асинхронные компоненты (Async Components) — паттерн React, при котором компонент самостоятельно выполняет асинхронные операции (загрузку данных, обращения к API, чтение файлов) непосредственно в своём теле, используя async/await. В экосистеме React Server Components (Next.js App Router, React 19) этот подход стал первоклассным инструментом для работы с данными на стороне сервера.

// Асинхронный серверный компонент — данные загружаются прямо в теле
async function UserProfile({ userId }) {
  const user = await fetchUser(userId);
  return <div>{user.name}</div>;
}

Server Components и async/await

До появления React Server Components все асинхронные операции приходилось выносить в хуки (useEffect, useState) или внешние стейт-менеджеры. Server Components позволяют писать компоненты с async/await напрямую — React выполнит их на сервере и отправит клиенту уже готовый HTML.

Основной синтаксис

// app/users/[id]/page.tsx (Next.js App Router)
async function UserPage({ params }: { params: { id: string } }) {
  // Прямой вызов БД или API — без useEffect, без useState
  const user = await db.user.findUnique({ where: { id: params.id } });

  if (!user) {
    notFound(); // Next.js утилита для 404
  }

  return (
    <main>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </main>
  );
}

export default UserPage;

Параллельная загрузка данных

// ❌ Последовательно — медленно (waterfall)
async function DashboardPage() {
  const user = await fetchUser();       // ждём...
  const posts = await fetchPosts();     // потом ждём...
  const stats = await fetchStats();     // и снова ждём...

  return <Dashboard user={user} posts={posts} stats={stats} />;
}

// ✅ Параллельно — быстро (Promise.all)
async function DashboardPage() {
  const [user, posts, stats] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchStats(),
  ]);

  return <Dashboard user={user} posts={posts} stats={stats} />;
}

Вложенные асинхронные компоненты

// Каждый компонент загружает только свои данные
async function PostList({ authorId }: { authorId: string }) {
  const posts = await fetchPostsByAuthor(authorId);

  return (
    <ul>
      {posts.map((post) => (
        <PostItem key={post.id} post={post} />
      ))}
    </ul>
  );
}

async function PostItem({ post }: { post: Post }) {
  const comments = await fetchComments(post.id);

  return (
    <li>
      <h3>{post.title}</h3>
      <span>{comments.length} комментариев</span>
    </li>
  );
}

Suspense — граница ожидания

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

Базовое использование

import { Suspense } from 'react';

function App() {
  return (
    <Suspense fallback={<p>Загрузка...</p>}>
      <UserProfile userId="42" />
    </Suspense>
  );
}

Стратегии размещения Suspense

// ✅ Один Suspense для всей страницы — простой подход
function Page() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <PageContent />
    </Suspense>
  );
}

// ✅ Вложенные Suspense — разные части страницы грузятся независимо
function Dashboard() {
  return (
    <div className="dashboard">
      {/* Шапка грузится сразу */}
      <Suspense fallback={<HeaderSkeleton />}>
        <DashboardHeader />
      </Suspense>

      <div className="dashboard-body">
        {/* Боковая панель и основной контент — независимо */}
        <Suspense fallback={<SidebarSkeleton />}>
          <Sidebar />
        </Suspense>

        <Suspense fallback={<ContentSkeleton />}>
          <MainContent />
        </Suspense>
      </div>
    </div>
  );
}

Skeleton-компоненты для fallback

function UserCardSkeleton() {
  return (
    <div className="user-card skeleton">
      <div className="skeleton-avatar" />
      <div className="skeleton-line" style={{ width: '60%' }} />
      <div className="skeleton-line" style={{ width: '80%' }} />
    </div>
  );
}

async function UserCard({ userId }: { userId: string }) {
  const user = await fetchUser(userId);

  return (
    <div className="user-card">
      <img src={user.avatar} alt={user.name} />
      <h3>{user.name}</h3>
      <p>{user.bio}</p>
    </div>
  );
}

// Использование
function UserSection({ userId }: { userId: string }) {
  return (
    <Suspense fallback={<UserCardSkeleton />}>
      <UserCard userId={userId} />
    </Suspense>
  );
}

Загрузка данных с async-компонентами

Fetch с кешированием (Next.js)

// Next.js автоматически дедуплицирует одинаковые запросы
async function ProductList() {
  // force-cache (по умолчанию) — кешируется между запросами
  const products = await fetch('/api/products', {
    next: { revalidate: 60 }, // ISR: обновлять раз в 60 секунд
  }).then((r) => r.json());

  return (
    <ul>
      {products.map((p: Product) => (
        <li key={p.id}>{p.name} — {p.price} ₽</li>
      ))}
    </ul>
  );
}

async function ProductDetail({ id }: { id: string }) {
  // no-store — не кешировать (всегда свежие данные)
  const product = await fetch(`/api/products/${id}`, {
    cache: 'no-store',
  }).then((r) => r.json());

  return <ProductView product={product} />;
}

Прямой доступ к базе данных

import { prisma } from '@/lib/prisma';

async function ArticleList() {
  // Серверный компонент — можно обращаться к БД напрямую
  const articles = await prisma.article.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' },
    take: 10,
    select: { id: true, title: true, slug: true, createdAt: true },
  });

  return (
    <section>
      {articles.map((article) => (
        <ArticleCard key={article.id} article={article} />
      ))}
    </section>
  );
}

Передача промисов в клиентские компоненты (React 19)

// page.tsx (Server Component)
import { use } from 'react';

async function Page() {
  // Передаём незавершённый промис — не ждём!
  const postsPromise = fetchPosts(); // Promise<Post[]>

  return (
    <Suspense fallback={<PostsSkeleton />}>
      <PostsClient postsPromise={postsPromise} />
    </Suspense>
  );
}

// PostsClient.tsx (Client Component)
'use client';

function PostsClient({ postsPromise }: { postsPromise: Promise<Post[]> }) {
  // use() разворачивает промис и интегрируется с Suspense
  const posts = use(postsPromise);

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

Обработка ошибок

Error Boundary

'use client';

import { Component, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    console.error('ErrorBoundary caught:', error, info);
  }

  render() {
    if (this.state.hasError) {
      return (
        this.props.fallback ?? (
          <div className="error-state">
            <h2>Что-то пошло не так</h2>
            <p>{this.state.error?.message}</p>
            <button onClick={() => this.setState({ hasError: false, error: null })}>
              Попробовать снова
            </button>
          </div>
        )
      );
    }
    return this.props.children;
  }
}

Комбинирование ErrorBoundary и Suspense

function SafeUserCard({ userId }: { userId: string }) {
  return (
    <ErrorBoundary
      fallback={
        <div className="error-card">
          Не удалось загрузить пользователя
        </div>
      }
    >
      <Suspense fallback={<UserCardSkeleton />}>
        <UserCard userId={userId} />
      </Suspense>
    </ErrorBoundary>
  );
}

error.tsx в Next.js App Router

// app/users/[id]/error.tsx
'use client';

export default function UserError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Ошибка загрузки пользователя</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Повторить</button>
    </div>
  );
}

Обработка ошибок внутри async-компонента

async function UserProfile({ userId }: { userId: string }) {
  try {
    const user = await fetchUser(userId);
    return <UserView user={user} />;
  } catch (error) {
    // Можно вернуть запасной UI прямо здесь
    // или пробросить ошибку в ErrorBoundary
    if (error instanceof NotFoundError) {
      return <p>Пользователь не найден</p>;
    }
    throw error; // Пробрасываем в ближайший ErrorBoundary
  }
}

Лучшие практики

1. Разделяй ответственность: Server vs Client

// ✅ Серверный компонент — только данные и разметка
async function ProductPage({ id }: { id: string }) {
  const product = await fetchProduct(id);
  return (
    <div>
      <ProductInfo product={product} />
      {/* Интерактивность — в клиентский компонент */}
      <AddToCartButton productId={product.id} />
    </div>
  );
}

// ✅ Клиентский компонент — только интерактивность
'use client';
function AddToCartButton({ productId }: { productId: string }) {
  const [added, setAdded] = useState(false);
  return (
    <button onClick={() => setAdded(true)}>
      {added ? 'Добавлено' : 'В корзину'}
    </button>
  );
}

2. Размещай Suspense как можно ближе к источнику данных

// ❌ Плохо — Suspense слишком высоко, блокирует всю страницу
function Page() {
  return (
    <Suspense fallback={<FullPageSpinner />}>
      <Header />        {/* Не требует данных */}
      <Navigation />    {/* Не требует данных */}
      <UserPosts />     {/* Требует данных */}
    </Suspense>
  );
}

// ✅ Хорошо — оборачиваем только то, что реально ждёт данные
function Page() {
  return (
    <>
      <Header />
      <Navigation />
      <Suspense fallback={<PostsSkeleton />}>
        <UserPosts />
      </Suspense>
    </>
  );
}

3. Используй generateStaticParams для статической генерации

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetchAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await fetchPostBySlug(params.slug);
  return <PostContent post={post} />;
}

export default BlogPost;

4. Не злоупотребляй глубиной вложенности

// ❌ Каскадные запросы — каждый ждёт предыдущего
async function OrderDetails({ orderId }: { orderId: string }) {
  const order = await fetchOrder(orderId);
  const user = await fetchUser(order.userId);     // ждёт order
  const items = await fetchOrderItems(order.id);  // ждёт order
  const shipping = await fetchShipping(order.id); // ждёт order

  return <OrderView order={order} user={user} items={items} shipping={shipping} />;
}

// ✅ Параллельные запросы там, где нет зависимостей
async function OrderDetails({ orderId }: { orderId: string }) {
  const order = await fetchOrder(orderId);

  // user, items и shipping не зависят друг от друга — грузим параллельно
  const [user, items, shipping] = await Promise.all([
    fetchUser(order.userId),
    fetchOrderItems(order.id),
    fetchShipping(order.id),
  ]);

  return <OrderView order={order} user={user} items={items} shipping={shipping} />;
}

5. TypeScript: типизируй пропсы и возвращаемые значения

import { Suspense } from 'react';

interface User {
  id: string;
  name: string;
  email: string;
  avatarUrl: string;
}

async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`Пользователь ${id} не найден`);
  return res.json();
}

// JSX.Element обычно выводится автоматически, но явная типизация помогает
async function UserCard({ userId }: { userId: string }): Promise<JSX.Element> {
  const user = await fetchUser(userId);
  return (
    <div className="user-card">
      <img src={user.avatarUrl} alt={user.name} />
      <strong>{user.name}</strong>
      <span>{user.email}</span>
    </div>
  );
}

Антипаттерны

useEffect для загрузки данных в Server Components

// ❌ Плохо — useEffect для загрузки данных устарел при наличии async-компонентов
'use client';
function UserList() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUsers().then((data) => {
      setUsers(data);
      setLoading(false);
    });
  }, []);

  if (loading) return <Spinner />;
  return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}

// ✅ Хорошо — async Server Component с Suspense
async function UserList() {
  const users = await fetchUsers();
  return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}

function UserSection() {
  return (
    <Suspense fallback={<Spinner />}>
      <UserList />
    </Suspense>
  );
}

Отсутствие обработки ошибок

// ❌ Плохо — нет обработки ошибок, приложение упадёт
async function Posts() {
  const posts = await fetchPosts(); // Что если API недоступен?
  return <PostList posts={posts} />;
}

// ✅ Хорошо — каждый уровень защищён
function PostsSection() {
  return (
    <ErrorBoundary fallback={<ErrorMessage />}>
      <Suspense fallback={<PostsSkeleton />}>
        <Posts />
      </Suspense>
    </ErrorBoundary>
  );
}

Передача функций и секретов клиенту через async-компоненты

// ❌ Плохо — приватные данные могут утечь на клиент
async function SecretComponent() {
  const data = await fetchSensitiveData(); // содержит токены, пароли и т.д.
  return <ClientComponent data={data} />; // всё пропсы сериализуются!
}

// ✅ Хорошо — передавай только то, что нужно для UI
async function SecretComponent() {
  const rawData = await fetchSensitiveData();
  const safeData = { id: rawData.id, displayName: rawData.displayName };
  return <ClientComponent data={safeData} />;
}

Сравнение подходов к загрузке данных

Подход Среда Когда использовать
async Server Component Сервер Основная загрузка данных, SEO, первичный рендер
use(promise) Клиент + Suspense Потоковая передача данных с сервера на клиент
useEffect + useState Клиент Данные, зависящие от действий пользователя
SWR / React Query Клиент Кеширование, фоновое обновление, мутации
getServerSideProps Сервер (Pages Router) Легаси Next.js Pages Router

Резюме

Асинхронные компоненты с async/await — современный стандарт работы с данными в React Server Components. Ключевые правила:

Правило Описание
async/await в теле Серверные компоненты поддерживают async нативно
Suspense для загрузки Оборачивай async-компоненты в Suspense с fallback
ErrorBoundary для ошибок Всегда добавляй обработку ошибок рядом с Suspense
Promise.all для параллельности Независимые запросы — параллельно
Минимальный client-bundle Переноси логику данных на сервер, клиенту — только UI

Дополнительные ресурсы

Стрелочка влевоCode Splitting в React: как уменьшить бандл и ускорить загрузку приложения

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

Uncontrolled Components: когда DOM управляет даннымиRender Props: гибкое управление рендерингом в ReactПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIКомпозиция компонентов в React: строим гибкие интерфейсыCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с данными
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseDeferredValue — отложенное обновление состоянияuseId — генерация уникальных идентификаторовuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияuseEffect в React что это и как использоватьuseContext — работа с контекстом в ReactОптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextКак предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
StrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOM
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий