В классических SPA-приложениях загрузка данных сопровождалась каскадом бойлерплейта: объявление useEffect, ручное управление флагами isLoading, isError, создание стейта data и предотвращение гонок запросов (Race Conditions). Это неизбежно порождало «водопады запросов» (Request Waterfalls), когда каждый дочерний компонент начинал загрузку только после того, как родитель завершил свою.
Next.js 16 и React 19 полностью трансформируют эту модель с помощью асинхронных серверных компонентов (async Server Components):
async/await прямо в теле компонента: компоненты объявляются как async function Page() и вызывают базу данных или внешние сервисы напрямую без промежуточных хуков.loading.tsx: создание файла loading.tsx автоматически оборачивает соседний page.tsx в границу React.Suspense. Браузер мгновенно получает статическую оболочку страницы и скелетон загрузки по протоколу HTTP/2 streaming, пока данные на сервере еще готовятся.error.tsx: файл error.tsx автоматически создает границу обработки ошибок (Error Boundary), предотвращая падение всего сайта при сбое одного блока.Взглянем на автоматическую иерархию компонентов в Next.js:
+-------------------------------------------------------------------------+
| АВТОМАТИЧЕСКАЯ ИЕРАРХИЯ ОБРАБОТКИ СОСТОЯНИЙ |
+-------------------------------------------------------------------------+
| <Layout> (app/layout.tsx) |
| <ErrorBoundary fallback={<Error />}> (app/error.tsx) |
| <Suspense fallback={<Loading />}> (app/loading.tsx) |
| <Page /> (app/page.tsx: async/await) |
| </Suspense> |
| </ErrorBoundary> |
| </Layout> |
+-------------------------------------------------------------------------+
| ЧТО ВИДИТ ПОЛЬЗОВАТЕЛЬ ПРИ ЗАПРОСЕ: |
| 0 мс: Мгновенно прилетает Layout + красивый Skeleton Loading! |
| 250 мс: Серверный промис разрешается -> Skeleton заменяется на UI|
| При сбое: Ошибка локализуется в Error Boundary с кнопкой "Повторить"|
+-------------------------------------------------------------------------+Пользователь переходит по ссылке. Сервер начинает параллельное выполнение layout.tsx и page.tsx.
Сервер мгновенно отправляет первый чанк HTML с разметкой layout.tsx и fallback из loading.tsx.
Серверный компонент ожидает завершения SQL-запроса или сетевого вызова fetch.
Как только данные готовы, сервер досылает готовый HTML страницы, и React заменяет скелетон на контент.
Почему в файле серверного компонента `app/users/page.tsx` можно написать `const users = await db.query('SELECT * FROM users');` прямо в теле функции без хуков useEffect и useState?
<Suspense>.reset): встроенная функция в error.tsx, позволяющая пользователю повторить попытку рендера без F5.Данные запрашиваются напрямую через стандартный fetch или ORM (Prisma, Drizzle):
interface Metric {
title: string;
count: number;
}
// Прямой асинхронный вызов к источнику данных
async function getMetrics(): Promise<Metric[]> {
const res = await fetch('https://api.wpskills.dev/v1/metrics', {
// В Next.js 16 fetch кэшируется согласно настройкам проекта
cache: 'no-store', // Всегда свежие данные для дашборда
});
if (!res.ok) {
// Выброс ошибки автоматически активирует error.tsx!
throw new Error('Не удалось загрузить аналитические метрики');
}
return res.json();
}
export default async function DashboardPage() {
const metrics = await getMetrics();
return (
<div className="space-y-6">
<h2 className="text-xl font-bold">Сводка показателей</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{metrics.map(m => (
<div key={m.title} className="p-4 rounded-xl border border-border bg-bg-surface">
<span className="text-xs text-text-muted">{m.title}</span>
<p className="text-2xl font-bold mt-1 text-accent-cyan">{m.count}</p>
</div>
))}
</div>
</div>
);
}loading.tsx#Next.js автоматически отображает этот компонент во время ожидания getMetrics():
export default function DashboardLoading() {
return (
<div className="space-y-6 animate-pulse">
{/* Скелетон заголовка */}
<div className="h-7 w-48 bg-border/60 rounded-lg" />
{/* Скелетон карточек */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{[1, 2, 3].map(i => (
<div key={i} className="p-4 rounded-xl border border-border bg-bg-surface/50 space-y-3">
<div className="h-4 w-24 bg-border/50 rounded" />
<div className="h-8 w-16 bg-border/80 rounded" />
</div>
))}
</div>
</div>
);
}error.tsx#Файл error.tsx обязан быть клиентским компонентом ('use client'), так как ему требуется интерактивная кнопка повтора (reset()):
'use client'; // ОБЯЗАТЕЛЬНО: Error Boundary всегда клиентский!
import React, { useEffect } from 'react';
interface ErrorProps {
error: Error & { digest?: string };
reset: () => void; // Функция для повторной попытки рендера
}
export default function DashboardError({ error, reset }: ErrorProps) {
useEffect(() => {
// Логирование ошибки в мониторинг (Sentry / Datadog)
console.error('Ошибка в сегменте дашборда:', error);
}, [error]);
return (
<div className="p-6 rounded-2xl border border-rose-500/30 bg-rose-500/10 text-center space-y-4 max-w-lg mx-auto">
<div className="text-rose-400 font-bold text-lg">
Произошла ошибка при загрузке данных
</div>
<p className="text-xs text-text-muted">
{error.message || 'Внутренний сбой сервера'}
</p>
<div>
<button
onClick={() => reset()}
className="px-4 py-2 rounded-lg bg-rose-500 text-white text-sm font-semibold hover:bg-rose-600 transition-colors"
>
Повторить попытку
</button>
</div>
</div>
);
}Почему файл error.tsx в Next.js App Router обязан содержать директиву 'use client' в первой строке?
Последовательный вызов независимых промисов замедляет загрузку:
// ПЛОХО: профиль ждет завершения настроек, общее время = 500мс + 500мс = 1с!
const profile = await getProfile();
const settings = await getSettings();
// ПРАВИЛЬНО: параллельный запуск через Promise.all (общее время = 500мс)!
const [profile, settings] = await Promise.all([getProfile(), getSettings()]);Если забыть 'use client' в error.tsx, Next.js выдаст ошибку компиляции:
Error: error.js must be a Client Component. Add the 'use client' directive to the top of the file.