Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «App Router vs Pages Router у Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**App Router vs Pages Router у Next.js**: Pages Router використовує директорію `pages/`, де компоненти запускаються в браузері, а серверні дані надходять через `getServerSideProps`. App Router використовує `app/`, де кожен компонент за замовчуванням React Server Component, дані фетчаться в `async`-компонентах, а layouts вкладаються через `layout.tsx`. Для нових проектів обирай App Router.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**App Router vs Pages Router у Next.js** - два роутери в одному фреймворку, які по-різному відповідають на одне питання: де відбувається рендеринг. Pages Router вважає, що роботу робить браузер і звертається до сервера за даними коли треба. App Router вважає, що роботу робить сервер, а браузер підключається за потребою. ## Теорія ### TL;DR - Аналогія: Pages Router - це ресторан, де ти замовляєш за столиком і чекаєш поки кухня все приготує. App Router - шведський стіл, де тарілки приходять вже зібраними з сервера. - Pages Router відправляє повний JS-бандл у браузер для гідратації. App Router стрімить server-rendered HTML і гідратує лише частини з `'use client'`. - Вся різниця API в одному рядку: `getServerSideProps` проти `async`-компонента. - Новий проект: App Router. Існуючий проект на Pages: мігруй поступово, один маршрут за раз. Обидва роутери живуть в одному репо. - Streaming, вкладені layouts, Server Actions і partial prerendering - все це є тільки в App Router. ### Швидкий приклад Pages Router фетчить дані поза компонентом і передає їх через props: ```tsx // pages/posts/[id].tsx export async function getServerSideProps({ params }) { const post = await fetch(`https://api.itlead.org/posts/${params.id}`).then(r => r.json()) return { props: { post } } // серіалізується в JSON, відправляється клієнту } export default function PostPage({ post }) { return <h1>{post.title}</h1> // запускається в браузері після повної гідратації } ``` App Router об'єднує фетчинг і рендеринг в одному компоненті: ```tsx // app/posts/[id]/page.tsx export default async function PostPage({ params }) { const post = await fetch(`https://api.itlead.org/posts/${params.id}`).then(r => r.json()) return <h1>{post.title}</h1> // стрімиться як HTML, без повної гідратації } ``` Версія App Router не має окремого helper-а і не має інтерфейсу для props. `fetch` запускається на сервері і ніколи не потрапляє в клієнтський бандл. ### Головна відмінність Pages Router трактує фетчинг даних як file-level експорт, який виконується до рендерингу компонента. Сам компонент завжди є клієнтським і відправляється в браузер. App Router робить async server-компоненти дефолтом: вони самі фетчать дані, запускаються на сервері і відправляють HTML. Браузер гідратує лише частини позначені `'use client'`. Решта залишається статичним markup-ом. ### Коли що обирати - Новий проект із SEO-вимогами: App Router. Server rendering плюс streaming дають кращий LCP на динамічному контенті. - Існуючий код до Next.js 13: залишайся на Pages Router і мігруй нові маршрути в `app/` поступово. - Статичний експорт (`output: 'export'`): тільки Pages Router. React Server Components несумісні з режимом статичного експорту. - Команда, яка ще не знайома з RSC: Pages Router легше пояснити, поки команда освоює нову модель. ### Таблиця порівняння | Фіча | Pages Router (`pages/`) | App Router (`app/`) | |---|---|---| | З'явився | Next.js 9 (2019), дефолт до 13 | Next.js 13 (2023), стабільний 13.4+, дефолт у 14+ | | Компонент за замовчуванням | Клієнтський | React Server Component | | Фетчинг даних | `getStaticProps`, `getServerSideProps` | `async`-компоненти з `fetch` | | Layouts | Глобальний `_app.tsx` | Вкладені `layout.tsx` на будь-якій глибині | | Loading UI | Ручне управління станом | `loading.tsx` із Suspense з коробки | | Обробка помилок | Глобальний `_error.tsx` | Per-route `error.tsx` | | Streaming | Не підтримується | Так, через `<Suspense>` і `loading.tsx` | | Metadata | `<Head>` компонент | `generateMetadata` на маршрут | | Мутації | API route плюс client `fetch` | Server Actions з `'use server'` | | Розмір бандлу | Більший (повна гідратація) | Менший (server-компоненти не йдуть у браузер) | | Кешування | Вручну | Автоматично через опції `fetch` | | Коли використовувати | Legacy-проекти, статичний експорт | Нові проекти, динамічний UI, колокація даних | ### Як обробляється запит Pages Router при SSR-запиті: запустити `getServerSideProps` на сервері, серіалізувати результат у JSON, відправити props клієнту, гідратувати все дерево компонентів через `hydrateRoot`. Кожна сторінка відправляє свій код у браузер, навіть якщо вміст ніколи не змінюється після завантаження. App Router працює інакше. Next.js сканує дерево `app/` під час білду і генерує route manifest. На запит: 1. Next.js проходить папки від кореня, збираючи кожен `layout.tsx` по шляху до знайденого маршруту. 2. Кожен компонент на цьому шляху запускається на сервері. Async-компоненти зупиняються на `await`, поки runtime стрімить вже готовий HTML у браузер. 3. Компоненти з `'use client'` серіалізуються з їхніми props у форматі RSC Payload (через React Flight по HTTP) і відправляються у браузер. 4. Браузер гідратує лише ці клієнтські компоненти. Server-компоненти залишаються чистим HTML і ніколи не потрапляють у React runtime браузера. Один практичний момент: `fetch` всередині App Router компонентів пропатчений Next.js. Однакові виклики fetch в кількох компонентах в рамках одного запиту дедуплікуються автоматично. Якщо два компоненти звертаються до одного URL, у мережу йде лише один запит. У Pages Router такого немає. ### Фетчинг даних як турбота компонента Pages Router прив'язує фетчинг до рівня файлу. Ти пишеш `getServerSideProps` або `getStaticProps` як іменований експорт, повертаєш `{ props }`, і сторінка отримує ці props. Це працювало, але обміняти fetch-логіку між сторінкою і глибоко вкладеним компонентом було незручно. Всі дані мали текти вниз через props від самого верху. App Router дозволяє будь-якому компоненту бути `async`. Layout може фетчити. Сторінка може фетчити. Глибоко вкладений server-компонент фетчить незалежно. Кожен `await` обробляється паралельно де можливо, і Next.js починає стрімити готові частини поки інші ще вантажаться. Дані живуть поруч із компонентом, який їх використовує. Важливо: відповіді `fetch` кешуються в App Router за замовчуванням. Використовуй `{ next: { revalidate: 3600 } }` для time-based ревалідації або `{ cache: 'no-store' }` коли потрібні свіжі дані кожного разу. ### Поширені помилки **Позначити весь файл сторінки як `'use client'`** ```tsx 'use client' // неправильно: вся сторінка стає клієнтським бандлом export default function Page() { const [open, setOpen] = useState(false) return <div>...</div> } ``` Це скасовує всі переваги RSC. Весь файл їде в браузер, розмір бандлу приблизно подвоюється, TTFB зростає бо сервер не може стрімити HTML до завантаження JS. Виреж стану частину в невеликий дочірній компонент і позначай `'use client'` лише його файл. Сама сторінка залишається server-компонентом. **Використовувати `useState` або `useEffect` в App Router-сторінці** ```tsx // неправильно: хуки недоступні в React Server Components export default function Page() { const [data, setData] = useState(null) useEffect(() => { fetchData().then(setData) }, []) // помилка білду } ``` Server-компоненти це `async`-функції, а не React-компоненти в традиційному розумінні. `useState`, `useEffect`, `useContext` - все це кидає помилку білду всередині `app/`. Використовуй `async`/`await`: ```tsx export default async function Page() { const data = await fetchData() // запускається на сервері, хуки не потрібні return <div>{data.title}</div> } ``` **Чекати що провайдери з `_app.tsx` будуть доступні в `app/`** У гібридному режимі `pages/_app.tsx` і `app/layout.tsx` запускаються в окремих контекстах. React Context provider, доданий до `_app.tsx`, не видимий жодному маршруту в `app/`. Auth-провайдери і theme-провайдери треба або дублювати, або повністю переносити до `app/layout.tsx` після завершення міграції. Це ловить майже кожну команду на першому гібридному проекті. **Пропустити `fallback` у динамічних маршрутах Pages Router** ```tsx export async function getStaticPaths() { return { paths: [...], fallback: false } // 404 для шляхів не збілджених заздалегідь } ``` `fallback: false` підходить для контенту, який не росте після деплою. Для всього що додає нові шляхи з часом використовуй `fallback: 'blocking'` щоб увімкнути генерацію на вимогу без спалаху loading-стану. ### Де зустрічається в реальних проектах - Дашборд Vercel: App Router із вкладеними layouts що шарять навігацію і streaming для live-метрик. - Linear: App Router parallel routes для модалок задач, що рендеряться поверх списку без перезавантаження сторінки. - Дашборд Supabase: App Router server `fetch` для realtime-запитів до бази, колокований із компонентами що їх показують. - T3 Stack (`create-t3-app`): App Router за замовчуванням з v10, в парі з tRPC і RSC. - Stripe Dashboard: гібридний режим, активно мігрує на App Router для payments UI що виграє від server-компонентів. ### Питання на співбесіді **Q:** Чому не можна використовувати `getServerSideProps` всередині `app/`? **A:** Це конвенція Pages Router, яку Next.js читає тільки в директорії `pages/`. Аналог в App Router - `async`-компонент, що фетчить свої дані напряму. Обидві конвенції свідомо розділені і не взаємозамінні. **Q:** Якщо я позначу компонент `'use client'`, він все ще може отримувати дані з сервера? **A:** Так. Server-компонент може рендерити `'use client'`-дитину і передавати їй server-fetched дані через props. Рядки, числа, прості об'єкти і масиви серіалізуються через межу без проблем. Функції не серіалізуються, тому fetch-функцію як prop передати не вийде. Для цього потрібен Server Action з `'use server'`. **Q:** Як Next.js дедуплікує fetch-запити в App Router? **A:** `fetch` пропатчений в контексті App Router. Виклики з однаковим URL і однаковими опціями в рамках одного рендер-пасу дедуплікуються автоматично. У мережу йде лише один запит, скільки б компонентів його не викликали. **Q:** Pages Router і App Router можуть постійно співіснувати в одному проекті? **A:** Технічно так, Next.js це підтримує. На практиці спільне використання auth-провайдерів, стану сесії і layout-логіки між двома роутерами швидко ускладнюється. Більшість команд трактують гібридний режим як тимчасовий стан міграції, а не постійну архітектуру. **Q:** Що ламається при деплої App Router на Edge Runtime? **A:** Node.js API недоступні. `fs`, `crypto` і все що залежить від Node internals кинуть помилку на Edge в runtime. Бібліотеки на кшталт Prisma потребують адаптерів типу `@prisma/adapter-edge`. Використовуй Web API або обмеж Edge-деплой маршрутами без Node-специфічного коду. ## Приклади ### Список постів у кожному роутері ```tsx // pages/posts/index.tsx (Pages Router) import type { GetServerSideProps } from 'next' type Post = { id: string; title: string } export const getServerSideProps: GetServerSideProps = async () => { const res = await fetch('https://api.itlead.org/posts') const posts: Post[] = await res.json() return { props: { posts } } } export default function PostsPage({ posts }: { posts: Post[] }) { return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ) } ``` ```tsx // app/posts/page.tsx (App Router) import { db } from '@/lib/db' export default async function PostsPage() { const posts = await db.post.findMany({ orderBy: { createdAt: 'desc' } }) return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ) } ``` Версія App Router не має exported helper-а, окремої функції для даних і props-інтерфейсу. Імпорт бази даних живе у файлі сторінки і залишається на сервері. Саме цей момент реально змінює розмір бандлу в продакшені. У Pages Router довелось би тримати `db` всередині `getServerSideProps` і передавати дані через props, щоб драйвер не потрапив у браузер. ### Вкладений layout зі стрімінгом ```tsx // app/dashboard/layout.tsx import { Sidebar } from '@/components/sidebar' export default function DashboardLayout({ children }: { children: React.ReactNode }) { return ( <div className="dashboard"> <Sidebar /> <main>{children}</main> </div> ) } ``` ```tsx // app/dashboard/loading.tsx import { DashboardSkeleton } from '@/components/dashboard-skeleton' export default function Loading() { return <DashboardSkeleton /> } ``` ```tsx // app/dashboard/page.tsx import { getRecentActivity } from '@/lib/activity' export default async function DashboardPage() { const activity = await getRecentActivity() return ( <section> <h1>Recent activity</h1> <ul> {activity.map((item) => ( <li key={item.id}>{item.description}</li> ))} </ul> </section> ) } ``` Коли користувач переходить на `/dashboard`, Next.js одразу рендерить layout і sidebar, потім показує `DashboardSkeleton` поки `getRecentActivity()` ще виконується. Коли дані прийшли, скелетон міняється на реальний контент. Sidebar не ре-рендериться, бо layouts зберігають стан під час навігації всередині свого піддерева. У Pages Router це все треба було б збирати вручну: `useState`, спінер і `useEffect` з fetch на mount. Три файли тут замінюють усе це, і кожен кешується незалежно. ### Server і client компоненти на одній сторінці Типовий сценарій зі співбесіди. Що тут потребує `'use client'`, а що ні? ```tsx // app/search/page.tsx import { searchPosts } from '@/lib/search' import { SearchInput } from './search-input' export default async function SearchPage({ searchParams, }: { searchParams: { q?: string } }) { const query = searchParams.q ?? '' const results = query ? await searchPosts(query) : [] return ( <div> <SearchInput initialQuery={query} /> <ul> {results.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> </div> ) } ``` ```tsx // app/search/search-input.tsx 'use client' import { useState } from 'react' import { useRouter } from 'next/navigation' export function SearchInput({ initialQuery }: { initialQuery: string }) { const [value, setValue] = useState(initialQuery) const router = useRouter() return ( <input value={value} onChange={(e) => setValue(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') router.push(`/search?q=${value}`) }} placeholder="Search..." /> ) } ``` Сама сторінка - server-компонент: читає `searchParams`, викликає `searchPosts` на сервері, рендерить список результатів. `SearchInput` потребує `'use client'` бо використовує `useState` і `useRouter`. Сторінка передає `initialQuery` як prop - це нормально, бо рядки серіалізуються через межу server/client без проблем. Те, що багато команд неправильно роблять на першій міграції: не можна передати сам `searchPosts` як prop клієнтському компоненту. Функції не серіалізуються. Якщо клієнтський компонент мав би викликати пошук напряму, треба загорнути `searchPosts` у Server Action через `'use server'` і передати action як prop.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.