Next.js App Router
Коротко: Система маршрутизации и отрисовки Next.js (с версии 13), в которой страницы по умолчанию отрисовываются на сервере («серверные компоненты» Server), а в браузере выполняются только явно помеченные части («клиентские компоненты» Client, "use client").
Подробнее: next/dynamic с ssr:false (полностью исключить компонент из серверной отрисовки) работает только внутри клиентского компонента — в серверном компоненте (например, layout.tsx без "use client") это жёсткая ошибка сборки, а не предупреждение. npx tsc --noEmit проверяет только типы TypeScript и не ловит такие специфичные для Next.js правила — для этого нужен настоящий next build.
Наш контекст: Ошибка ssr:false именно такого рода однажды сломала сборку — с тех пор перед каждой крупной партией запускается настоящий npm run build, а не только проверка TypeScript.
Подробности
Маршрутизация на основе файлов через особые имена файлов
App Router организует маршруты через структуру папок в app/ — каждая папка соответствует сегменту URL, а файл page.tsx в ней превращает папку в настоящую доступную страницу (папки без page.tsx чисто структурные, например для общих файлов layout.tsx). Это принципиальное отличие от старого Pages Router (каталог pages/), где каждый файл автоматически был маршрутом, а макеты приходилось импортировать вручную на каждую страницу. Помимо page.tsx существует целый ряд других зарезервированных имён файлов с фиксированным значением: layout.tsx оборачивает все страницы под ним и сохраняется при навигации внутри того же макета (без повторного монтирования), loading.tsx автоматически показывается как запасной вариант Suspense, пока загружается серверный компонент, error.tsx перехватывает ошибки времени выполнения в своём поддереве (сам должен быть клиентским компонентом), а not-found.tsx отображается при вызове notFound(). Папки в квадратных скобках ([slug]) создают динамические сегменты маршрута; квадратные скобки с тремя точками ([...slug]) захватывают любое число сегментов пути сразу («catch-all» маршруты).
Серверные компоненты по умолчанию
Серверные компоненты — значение по умолчанию: они выполняются исключительно на сервере, имеют прямой доступ к базам данных/секретам, но САМИ не отправляют в браузер никакого кода JavaScript — результат уже готовый HTML. Клиентские компоненты ("use client" в начале файла), напротив, дополнительно отправляются в браузер как JavaScript и там «гидратируются», чтобы стать интерактивными (onClick, useState, useEffect работают только там). Практическое правило: оставляйте как можно больше серверными компонентами и добавляйте "use client" только там, где нужна настоящая интерактивность в браузере — каждый клиентский компонент увеличивает пакет JavaScript, отправляемый в браузер. Важно: директива "use client" помечает не один компонент, а всю границу модуля — каждый компонент, импортированный клиентским компонентом, тоже становится частью клиентского пакета, даже если сам не использует useState.
// app/posts/[slug]/page.tsx — серверный компонент (по умолчанию, без "use client")
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await db.query.posts.findFirst({ where: eq(posts.slug, params.slug) });
return <article>{post?.title}</article>;
}Распространённый шаблон — сочетание серверных и клиентских компонентов путём встраивания небольшого интерактивного клиентского компонента как пропа children в серверный компонент — серверный компонент остаётся только серверным, хотя «содержит» клиентский:
// app/layout.tsx — серверный компонент, отрисовывает клиентский компонент как дочерний
import { ThemeToggle } from "./theme-toggle"; // клиентский компонент
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<ThemeToggle />
{children}
</body>
</html>
);
}Загрузка данных и кэширование
Серверные компоненты могут напрямую использовать async/await для загрузки данных — не нужны ни useEffect, ни отдельное состояние загрузки в клиенте, поскольку ожидание происходит на сервере до отправки какого-либо HTML в браузер. Next.js расширяет нативный API fetch собственным поведением кэширования: результаты по умолчанию кэшируются и повторно используются между сборками, если явно не задано cache: "no-store" или опция revalidate. Это частый источник ошибок для новичков, пришедших из классического React без серверной отрисовки и ожидающих неявного «перезагружать при каждом запросе».
Типичные подводные камни
Ошибка ssr:false с next/dynamic (см. выше) — лишь одно из нескольких специфичных для Next.js правил, которые tsc не обнаруживает — смешивание API только для сервера и только для клиента (например, window в серверном компоненте) тоже замечается лишь при next build или во время выполнения. Ещё одна классическая ошибка: пропы, передаваемые из серверного компонента в клиентский, должны быть сериализуемыми (никаких функций, экземпляров классов), поскольку их фактически нужно «перевезти» через границу сервера/клиента.
См. также: next-intl, Теги кэша и revalidateTag