EMZETT.
Увійти

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