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 Components | Server Components за замовчуванням |
| Data fetching | getStaticProps, getServerSideProps | async компоненти, 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 Component | Client 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.
Корисні посилання:
- Next.js: Server Components -- офіційна документація
- Next.js: Client Components -- коли і як використовувати
- React: Server Components -- документація React