Урок 06

Loading и Error

Next.js предоставляет специальные файлы для состояний загрузки и ошибок.

loading.tsx

Показывается, пока страница грузится. Автоматически оборачивает в <Suspense>.

// app/posts/loading.tsx
export default function Loading() {
    return (
        <div className="skeleton">
            <div className="skeleton-line" />
            <div className="skeleton-line" />
            <div className="skeleton-line" />
        </div>
    );
}

Работает для всех страниц в папке.

error.tsx

Ловит ошибки рендера и в дочерних компонентах.

'use client';

export default function Error({
    error,
    reset,
}: {
    error: Error & { digest?: string };
    reset: () => void;
}) {
    return (
        <div>
            <h2>Что-то пошло не так</h2>
            <p>{error.message}</p>
            <button onClick={reset}>Попробовать снова</button>
        </div>
    );
}

Должен быть клиентским компонентом.

reset() — попытка перерендерить сегмент.

global-error.tsx

Ловит ошибки в root layout:

'use client';

export default function GlobalError({
    error,
    reset,
}: {
    error: Error;
    reset: () => void;
}) {
    return (
        <html>
            <body>
                <h2>Критическая ошибка</h2>
                <button onClick={reset}>Перезагрузить</button>
            </body>
        </html>
    );
}

Должен содержать <html> и <body>.

not-found.tsx

Показывается при notFound() или 404:

// app/not-found.tsx
import Link from 'next/link';

export default function NotFound() {
    return (
        <div>
            <h1>404</h1>
            <p>Страница не найдена</p>
            <Link href="/">На главную</Link>
        </div>
    );
}

Вложенный:

// app/posts/[id]/not-found.tsx
export default function PostNotFound() {
    return <p>Пост не найден</p>;
}

Показывается для 404 внутри /posts/[id].

notFound()

import { notFound } from 'next/navigation';

const post = await getPost(id);
if (!post) notFound();

Показывает ближайший not-found.tsx.

Иерархия

app/
├── layout.tsx
├── loading.tsx              ← для /
├── error.tsx                ← для /
├── not-found.tsx            ← для /
├── posts/
│   ├── loading.tsx          ← для /posts
│   ├── error.tsx            ← для /posts
│   └── [id]/
│       ├── loading.tsx      ← для /posts/[id]
│       └── not-found.tsx    ← для /posts/[id]

Каждый уровень может иметь свои.

Suspense

Более тонкий контроль:

import { Suspense } from 'react';

export default function Page() {
    return (
        <>
            <h1>Заголовок</h1>
            <Suspense fallback={<Skeleton />}>
                <SlowComponent />
            </Suspense>
            <Suspense fallback={<p>Загрузка комментариев...</p>}>
                <Comments />
            </Suspense>
        </>
    );
}

Каждый блок грузится отдельно.

Streaming

Next.js стримит HTML по мере готовности. Пользователь видит части страницы до полной загрузки.

export default function Page() {
    return (
        <>
            <Header /> {/* сразу */}
            <Suspense fallback={<Spinner />}>
                <SlowData /> {/* по мере готовности */}
            </Suspense>
        </>
    );
}

Обработка ошибок в Server Actions

'use server';

export async function createPost(prevState, formData: FormData) {
    try {
        await db.post.create({ data: { title: formData.get('title') } });
        revalidatePath('/posts');
        return { success: true };
    } catch (error) {
        return { error: 'Не удалось создать' };
    }
}

Ошибки возвращаются как состояние формы, а не через error.tsx.

Логирование ошибок

'use client';

import { useEffect } from 'react';

export default function Error({ error, reset }) {
    useEffect(() => {
        console.error('Ошибка:', error);
        // отправить в Sentry
    }, [error]);

    return (
        <div>
            <h2>Ошибка</h2>
            <button onClick={reset}>Повторить</button>
        </div>
    );
}

Итоги

  • loading.tsx — состояние загрузки
  • error.tsx — обработка ошибок
  • global-error.tsx — для root layout
  • not-found.tsx — 404
  • notFound() для ручного вызова
  • Suspense для тонкого контроля
  • Streaming показывает части страницы