У класичних 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.