Вивчай

App Router

Next.js App Router — сучасний офіс де серверна кімната і дизайн-студія з'єднані скляним коридоромNext.js App Router — сучасний офіс де серверна кімната і дизайн-студія з'єднані скляним коридором

У попередніх уроках ми працювали з Pages Router (директорія pages/) та App Router. Тепер зосередимося на App Router -- сучасній архітектурі Next.js, яка принципово змінює підхід до побудови додатків. Головна інновація -- Server Components за замовчуванням.


App Router vs Pages Router

АспектPages Router (pages/)App Router (app/)
КомпонентиТільки Client ComponentsServer Components за замовчуванням
Data fetchinggetStaticProps, getServerSidePropsasync компоненти, fetch
LayoutsЧерез _app.tsx (перерендер)Вкладені layouts (persistent)
Loading statesВручнуВбудований loading.tsx
Error handling_error.tsxВбудований error.tsx
StreamingНіТак (Suspense)
Інфо

App Router -- це рекомендований підхід для нових проектів з Next.js 13+. Pages Router все ще підтримується і буде підтримуватися довго, але нові фічі додаються тільки в App Router.


Layouts -- постійні обгортки

Layout -- це компонент, який обгортає сторінки і зберігає стан між навігаціями:

// src/app/layout.tsx -- кореневий layout (обов'язковий)
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "Мій додаток",
  description: "Створений на Next.js",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="uk">
      <body>
        <header>
          <nav>Навігація сайту</nav>
        </header>
        {children}
        <footer>Футер</footer>
      </body>
    </html>
  );
}

Вкладені layouts

Кожна директорія може мати свій layout:

src/app/
  layout.tsx              ← Кореневий (header + footer)
  page.tsx                ← /
  blog/
    layout.tsx            ← Layout для блогу (sidebar)
    page.tsx              ← /blog
    [slug]/
      page.tsx            ← /blog/my-post
  dashboard/
    layout.tsx            ← Layout для дашборду (бокова панель)
    page.tsx              ← /dashboard
    settings/
      page.tsx            ← /dashboard/settings
// src/app/blog/layout.tsx
export default function BlogLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div style={{ display: "flex" }}>
      <aside style={{ width: "250px" }}>
        <h3>Блог</h3>
        <ul>
          <li>Категорія 1</li>
          <li>Категорія 2</li>
          <li>Категорія 3</li>
        </ul>
      </aside>
      <main style={{ flex: 1 }}>{children}</main>
    </div>
  );
}

Ключова перевага: при навігації між /blog/post-1 та /blog/post-2 Blog Layout не перерендерюється -- оновлюється тільки children.

template.tsx -- layout без збереження стану

Якщо потрібен layout, який перемонтовується при кожній навігації (наприклад, для анімацій входу):

// src/app/blog/template.tsx
export default function BlogTemplate({
  children,
}: {
  children: React.ReactNode;
}) {
  return <div className="fade-in">{children}</div>;
}

Server Components vs Client Components

Це найважливіша концепція App Router. Зрозумій її -- і все стане на свої місця.

Server Components (за замовчуванням)

У App Router кожен компонент -- це Server Component, якщо не вказано інше:

// src/app/users/page.tsx -- Server Component (за замовчуванням)

interface User {
  id: number;
  name: string;
  email: string;
}

export default async function UsersPage() {
  // Можна використовувати await прямо в компоненті!
  const res = await fetch("https://jsonplaceholder.typicode.com/users");
  const users: User[] = await res.json();

  return (
    <div>
      <h1>Користувачі</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name} -- {user.email}</li>
        ))}
      </ul>
    </div>
  );
}

Що можуть Server Components:

  • async/await прямо в компоненті
  • Доступ до файлової системи (fs)
  • Доступ до бази даних напряму
  • Приховування секретних ключів
  • Менший JavaScript-бандл (код не відправляється клієнту)

Що НЕ можуть Server Components:

  • useState, useEffect та інші хуки
  • Event handlers (onClick, onChange)
  • Browser API (localStorage, window)

Client Components

Для інтерактивності додай директиву "use client" на початку файлу:

// src/components/Counter.tsx
"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Лічильник: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

Що можуть Client Components:

  • useState, useEffect, useRef та всі хуки
  • Event handlers
  • Browser API
  • Третьосторонні бібліотеки з хуками

Що НЕ можуть Client Components:

  • async компоненти (async/await на верхньому рівні)
  • Прямий доступ до файлової системи або БД
Увага

"use client" -- це межа. Коли ти позначаєш компонент як Client, всі його дочірні компоненти теж стають клієнтськими. Тому розміщуй "use client" якнайнижче в дереві компонентів.


Коли що використовувати?

ЗадачаServer ComponentClient Component
Отримання даних (fetch)Можна, але краще на сервері
Доступ до БД
Секретні ключі
useState, useEffect
onClick, onChange
localStorage, window
Статичний контент✅ Менше JSМожна, але зайвий JS
Порада

Правило: починай з Server Components. Переходь на Client тільки коли потрібна інтерактивність. Це зменшує розмір JavaScript-бандлу і покращує швидкість завантаження.


Паттерн: комбінування Server та Client

Найпоширеніший паттерн -- Server Component отримує дані, а Client Component їх відображає інтерактивно:

// src/app/products/page.tsx -- Server Component
import ProductList from "@/components/ProductList";

interface Product {
  id: number;
  name: string;
  price: number;
}

export default async function ProductsPage() {
  const res = await fetch("https://api.example.com/products");
  const products: Product[] = await res.json();

  // Передаємо дані в Client Component
  return <ProductList products={products} />;
}
// src/components/ProductList.tsx -- Client Component
"use client";

import { useState } from "react";

interface Product {
  id: number;
  name: string;
  price: number;
}

export default function ProductList({ products }: { products: Product[] }) {
  const [search, setSearch] = useState("");

  const filtered = products.filter((p) =>
    p.name.toLowerCase().includes(search.toLowerCase())
  );

  return (
    <div>
      <input
        value={search}
        onChange={(e) => setSearch(e.target.value)}
        placeholder="Пошук..."
      />
      <ul>
        {filtered.map((p) => (
          <li key={p.id}>
            {p.name} -- {p.price} грн
          </li>
        ))}
      </ul>
    </div>
  );
}

Streaming з Suspense

App Router підтримує streaming -- поступове відправлення HTML по мірі готовності:

// src/app/dashboard/page.tsx
import { Suspense } from "react";

export default function DashboardPage() {
  return (
    <div>
      <h1>Дашборд</h1>

      <Suspense fallback={<p>Завантаження статистики...</p>}>
        <Stats />
      </Suspense>

      <Suspense fallback={<p>Завантаження графіку...</p>}>
        <Chart />
      </Suspense>

      <Suspense fallback={<p>Завантаження останніх дій...</p>}>
        <RecentActivity />
      </Suspense>
    </div>
  );
}

// Кожен компонент завантажується незалежно
async function Stats() {
  const data = await fetch("https://api.example.com/stats");
  const stats = await data.json();
  return <div>Статистика: {JSON.stringify(stats)}</div>;
}

async function Chart() {
  // Цей запит може бути повільним -- інші секції не чекатимуть
  const data = await fetch("https://api.example.com/chart");
  const chart = await data.json();
  return <div>Графік: {JSON.stringify(chart)}</div>;
}

async function RecentActivity() {
  const data = await fetch("https://api.example.com/activity");
  const activity = await data.json();
  return <div>Остання активність: {JSON.stringify(activity)}</div>;
}

Кожен Suspense boundary рендериться незалежно. Якщо Chart завантажується 3 секунди, користувач вже бачить Stats і інші секції.


Підсумок

  • App Router -- сучасний підхід у Next.js зі Server Components за замовчуванням
  • Layouts зберігають стан між навігаціями (header, sidebar не перерендерюються)
  • Server Components -- для отримання даних, доступу до БД, зменшення JS-бандлу
  • Client Components ("use client") -- для інтерактивності (хуки, події)
  • Комбінуй: Server Component отримує дані → передає в Client Component
  • Suspense + streaming -- поступове завантаження секцій сторінки

Що далі?

У наступному уроці детальніше розберемо Data Fetching в App Router -- кешування, revalidation, parallel fetching та Server Actions.

Інфо

Корисні посилання: