Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Ключові особливості Next.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Next.js** - React-фреймворк з вибором стратегії рендерингу на рівні маршруту: SSG генерує HTML при збірці, SSR - при кожному запиті, ISR - за розкладом, CSR - у браузері. ```tsx export const revalidate = 3600; // ISR const fresh = await fetch('/api/user', { cache: 'no-store' }); // SSR const cached = await fetch('/api/posts', { cache: 'force-cache' }); // SSG ``` **Ключове:** App Router (v13+) керує рендерингом через опції fetch. Всі компоненти - серверні за замовчуванням, що зменшує бандл і дає прямий доступ до БД без API-маршрутів.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Next.js** - React-фреймворк, який дозволяє обирати стратегію рендерингу для кожного маршруту окремо: SSG, SSR, ISR або CSR залежно від того, яких даних потребує сторінка. ## Теорія ### Коротко - SSG генерує HTML при збірці і роздає з CDN. Найшвидший варіант, але дані "замерзають" до наступного деплою. - SSR рендерить HTML на сервері при кожному запиті. Завжди актуальні дані, але є затримка сервера. - ISR - щось між: статичний HTML, який перебудовується у фоні за розкладом. - App Router (v13+) прибрав `getStaticProps`/`getServerSideProps`. Тепер усе контролюють опції `fetch` в async-компонентах. - Всі компоненти - серверні за замовчуванням. `'use client'` додаєш тільки там, де потрібен стан або браузерні API. ### Гібридний рендеринг Суть ідеї: один застосунок, кілька стратегій рендерингу. Ти обираєш для кожного маршруту. ```tsx // SSG - HTML генерується раз при збірці, роздається з CDN async function BlogPage() { const res = await fetch('https://api.itlead.org/posts', { cache: 'force-cache' // поведінка за замовчуванням в App Router }); const posts = await res.json(); return <PostList posts={posts} />; } // ISR - статичний при збірці, перебудовується у фоні кожні 5 хвилин export const revalidate = 300; async function ProblemsPage() { const res = await fetch('https://api.itlead.org/problems'); const problems = await res.json(); return <ProblemList problems={problems} />; } // SSR - свіжий HTML при кожному запиті async function DashboardPage() { const res = await fetch('https://api.itlead.org/user/stats', { cache: 'no-store' }); const stats = await res.json(); return <StatsPanel stats={stats} />; } ``` Правило вибору: SSG для всього, що однакове для кожного відвідувача; SSR для персоналізованих або реалтаймових даних; ISR для контенту, який змінюється, але не щосекунди. На практиці найбільший перехід з Create React App - це відмова від ідеї, що весь застосунок має одну стратегію рендерингу. ### App Router і маршрутизація через файли Next.js 13 представив App Router. Структура папок всередині `app/` безпосередньо визначає маршрути. Спеціальні файли відповідають за конкретні задачі без будь-якої конфігурації: | Файл | Призначення | |------|-------------| | `page.tsx` | UI маршруту, робить сегмент публічним | | `layout.tsx` | Обгортка, яка зберігається між навігаціями | | `loading.tsx` | UI завантаження на основі Suspense | | `error.tsx` | Error boundary для сегменту | | `not-found.tsx` | UI для 404 | ```tsx // app/docs/[slug]/page.tsx // Обробляє: /docs/javascript, /docs/react, /docs/nextjs export default async function DocPage({ params }: { params: { slug: string } }) { const doc = await getDocument(params.slug); return <article>{doc.content}</article>; } ``` Головна зміна порівняно з Pages Router: `getStaticProps` і `getServerSideProps` зникли. Їх замінили опції `fetch` всередині async Server Components. Менше шаблонного коду, той самий рівень контролю. ### Server Components (серверні компоненти) Кожен компонент в App Router є серверним за замовчуванням. Його код виконується на сервері, ніколи не потрапляє в браузерний бандл і може безпосередньо звертатися до бази даних, змінних середовища та файлової системи. ```tsx // app/stats/page.tsx - нульовий вплив на клієнтський бандл import { db } from '@/lib/db'; export default async function StatsPage() { const totalUsers = await db.user.count(); const totalProblems = await db.problem.count(); return ( <div> <p>Користувачів: {totalUsers}</p> <p>Задач: {totalProblems}</p> </div> ); } ``` Коли потрібна інтерактивність - стан, обробники подій, браузерні API - додаєш `'use client'` на початок файлу. Патерн, який добре працює в продакшені: завантаження даних залишається в серверних компонентах, інтерактивна логіка виноситься в невеликі клієнтські дочірні компоненти. ```tsx 'use client' import { useState } from 'react' export default function ThemeToggle() { const [dark, setDark] = useState(false) return ( <button onClick={() => setDark(!dark)}> {dark ? 'Світла тема' : 'Темна тема'} </button> ) } ``` ### Server Actions (серверні дії) Server Actions дозволяють викликати серверні функції прямо з клієнтських компонентів без написання окремих API-маршрутів. Позначаєш функцію директивою `'use server'` і викликаєш як звичайну функцію. ```tsx // actions/subscribe.ts 'use server' import { db } from '@/lib/db' import { revalidatePath } from 'next/cache' export async function subscribe(email: string) { await db.subscriber.create({ data: { email } }) revalidatePath('/newsletter') } ``` ```tsx // Клієнтський компонент - окремий API-маршрут не потрібен 'use client' import { subscribe } from '@/actions/subscribe' export default function SubscribeForm() { return ( <form action={async (formData) => { const email = formData.get('email') as string await subscribe(email) }}> <input name="email" type="email" placeholder="Email" /> <button type="submit">Підписатися</button> </form> ) } ``` Під капотом Next.js перетворює Server Action на захищений POST-запит. Типобезпека через межу клієнт-сервер без ручного написання API. ### Вкладені макети (Nested Layouts) Макети в App Router зберігаються між навігаціями. Компонент монтується один раз і залишається змонтованим, поки користувач переходить між дочірніми маршрутами. ```tsx // app/docs/layout.tsx - обгортає всі /docs/* маршрути export default function DocsLayout({ children }: { children: React.ReactNode }) { return ( <div className="flex"> <Sidebar /> {/* залишається змонтованим, зберігає позицію скролу */} <main className="flex-1">{children}</main> </div> ) } ``` Перехід з `/docs/javascript` на `/docs/react`? Бокова панель не ремонтується. Позиція скролу, відкриті секції, локальний стан макету - все зберігається. Перерисовується тільки `children`. Саме для цього і потрібні вкладені макети. ### Вбудовані оптимізації Три компоненти закривають більшість питань продуктивності з коробки. **`next/image`** змінює розміри зображень, конвертує у WebP або AVIF і додає lazy loading автоматично. Ти вказуєш розміри, решту робить компонент. ```tsx import Image from 'next/image' export default function Avatar() { return ( <Image src="/avatar.png" width={64} height={64} alt="Аватар користувача" /> ) } ``` **`next/link`** попередньо завантажує сторінки у фоні, коли посилання потрапляє у видиму область. Коли користувач клікає - сторінка вже завантажена. ```tsx import Link from 'next/link' export default function Nav() { return ( <nav> <Link href="/problems">Задачі</Link> <Link href="/docs">Документація</Link> </nav> ) } ``` **`next/font`** завантажує шрифти Google Fonts під час збірки і хостить їх самостійно. Жодних клієнтських запитів до серверів Google, жодного зсуву макету через пізнє завантаження шрифтів. ```tsx import { Inter } from 'next/font/google' const inter = Inter({ subsets: ['latin', 'cyrillic'] }) export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html className={inter.className}> <body>{children}</body> </html> ) } ``` ### Middleware Middleware виконується на Edge перед тим, як запит досягає застосунку. Можна робити редиректи, переписувати URL або змінювати заголовки. Edge Runtime - це не Node.js, тому `fs`, `path` та інші Node-специфічні API тут недоступні. ```tsx // middleware.ts (корінь проекту) import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' export function middleware(request: NextRequest) { const locale = request.cookies.get('locale')?.value || 'en' if (!request.nextUrl.pathname.startsWith(`/${locale}`)) { return NextResponse.redirect( new URL(`/${locale}${request.nextUrl.pathname}`, request.url) ) } } export const config = { matcher: ['/((?!api|_next|favicon.*))'] } ``` Типові сценарії: перевірка автентифікації, визначення локалі, редиректи для A/B тестів, додавання заголовків безпеки. ### API метаданих Next.js має вбудований API для SEO-метаданих. Експортуєш об'єкт `metadata` з будь-якого `page.tsx` або `layout.tsx`: ```tsx import type { Metadata } from 'next' export const metadata: Metadata = { title: 'JavaScript Tasks — IT Lead', description: 'Розвязуй задачі з реальних фронтенд-співбесід', openGraph: { title: 'JavaScript Tasks', description: 'Розвязуй задачі з реальних фронтенд-співбесід', type: 'website' } } export default function ProblemsPage() { return <ProblemsList /> } ``` Для динамічних метаданих (блог-пости, сторінки товарів) замість `metadata` експортуєш асинхронну функцію `generateMetadata`. Вона отримує параметри маршруту і може підтягувати дані для формування заголовку і опису. ### Типові помилки **Забути `cache: 'no-store'` на SSR-сторінках.** App Router кешує `fetch`-запити за замовчуванням. Дашборд, який викликає `fetch('/api/user')` без опцій, закешує відповідь при збірці і роздаватиме однакові дані всім. ```tsx // Неправильно - кешується як SSG, дає застарілі дані const data = await fetch('/api/user'); // Правильно - свіжі дані при кожному запиті const data = await fetch('/api/user', { cache: 'no-store' }); ``` **Додавати `'use client'` на всю сторінку заради одного інтерактивного елемента.** Це переводить всю сторінку на CSR і прибирає серверний HTML. Стан і обробники подій треба виносити в невеликі дочірні клієнтські компоненти. ```tsx // Неправильно - вся сторінка стає CSR, SEO зникає 'use client' export default async function Page() { ... } // Правильно - сторінка серверна, інтерактивність ізольована export default async function Page() { const data = await fetchData(); return <InteractiveChild data={data} />; } ``` **Думати, що `revalidate: 0` вмикає SSR.** `export const revalidate = 0` не робить маршрут серверним. Сторінка все одно статична при збірці. Для реального рендерингу при кожному запиті потрібен `export const dynamic = 'force-dynamic'` або `cache: 'no-store'` у fetch-запитах. **Пропускати `<Suspense>` навколо компонентів, що стрімлять.** Без Suspense-межі асинхронний Server Component блокує відправку будь-якого HTML до клієнта, поки не вирішиться. Загортай повільні компоненти в `<Suspense fallback={<Loader />}>`. ### Де зустрічається в реальних проектах - Vercel.com: SSG для документації (CDN по всьому світу), SSR для авторизованого дашборду. - Hashnode: SSG для постів, SSR для коментарів і персоналізованих стрічок. - E-commerce: ISR для сторінок товарів з `revalidate: 60`, щоб ціни оновлювалися без деплою. - Адмін-панелі: CSR з `'use client'` по всьому застосунку, бо SEO не важливий, а дані змінюються постійно. ### Питання на співбесіді **Q:** В чому різниця між `cache: 'no-store'` і `export const dynamic = 'force-dynamic'`? **A:** `cache: 'no-store'` застосовується до одного fetch-запиту. `dynamic = 'force-dynamic'` позначає весь маршрут як динамічний і поширюється на всі дочірні компоненти, перевизначаючи будь-яке кешування на рівні маршруту. **Q:** Як Turbopack впливає на час розробки? **A:** Turbopack замінює Webpack як бандлер у режимі розробки. Next.js 14 дає приблизно в 700 разів швидше оновлення модулів (HMR) у dev. Продакшн-збірки в Next.js 14 все ще використовують Webpack. **Q:** Що таке RSC payload і як він пов'язаний зі стрімінгом? **A:** React Server Components серіалізують свій вивід у RSC payload, а не в чистий HTML. Клієнт спочатку отримує статичну HTML-оболонку, потім RSC payload стрімиться для гідратації інтерактивних частин. `<Suspense>` дозволяє відправити оболонку одразу, а повільні частини - після їх вирішення. **Q:** Коли виникає hydration mismatch (розбіжність гідратації) і як його виправити? **A:** Коли серверний HTML не збігається з тим, що React очікує відрендерити на клієнті. Часті причини: часові мітки, випадкові ID, браузерні API під час рендеру. Вирішення: `useEffect` для відкладення клієнтського коду або `suppressHydrationWarning` для нешкідливих розбіжностей. **Q:** Як би ти інвалідував ISR-кеш у багаторегіональному застосунку з високим навантаженням? **A:** Через `revalidateTag` або `revalidatePath`, що тригеряться вебхуком при зміні контенту. Разом з Upstash Redis як спільним кеш-сховищем між Edge-регіонами. Для маршрутів, де застарілі дані неприпустимі, переключайся на SSR з `cache: 'no-store'`. ## Приклади ### ISR-сторінка продукту Сторінка товару, яка перебудовується у фоні кожні 60 секунд. Користувачі завжди отримують валідну закешовану відповідь. Оновлення відбувається без деплою. ```tsx // app/products/[id]/page.tsx interface Product { id: string; name: string; price: number; } export const revalidate = 60; export default async function ProductPage({ params }: { params: { id: string } }) { const product: Product = await fetch( `https://api.example.com/products/${params.id}`, { next: { revalidate: 60 } } ).then(res => res.json()); return ( <div> <h1>{product.name}</h1> <p>{product.price} грн</p> </div> ); } // Статичний HTML з CDN, дані товару оновлюються у фоні - без перезбірки ``` ### Стрімінговий дашборд Заголовок рендериться одразу. Секція аналітики стрімиться після того, як її дані вирішаться. Без `<Suspense>` вся сторінка чекає на найповільніший компонент. ```tsx // app/dashboard/page.tsx import { Suspense } from 'react' import Analytics from './analytics' export default function Dashboard() { return ( <> <Header /> {/* видно через ~50мс */} <Suspense fallback={<div>Завантажую аналітику...</div>}> <Analytics /> {/* стрімиться через ~2с */} </Suspense> </> ) } // app/dashboard/analytics.tsx export default async function Analytics() { const data = await fetch('https://api.itlead.org/analytics', { cache: 'no-store' }).then(r => r.json()); return <Chart data={data} />; } // Без спінера на всю сторінку. Оболонка видна, поки аналітика завантажується. ``` ### Server Action з інвалідацією кешу Форма записує в базу даних та інвалідує відповідний кешований маршрут. Окремий API-файл не потрібен. ```tsx // actions/solution.ts 'use server' import { db } from '@/lib/db' import { revalidatePath } from 'next/cache' export async function submitSolution(formData: FormData) { const code = formData.get('code') as string const problemId = formData.get('problemId') as string await db.solution.create({ data: { code, problemId } }) revalidatePath(`/problems/${problemId}`) } ``` ```tsx // app/problems/[id]/submit.tsx 'use client' import { submitSolution } from '@/actions/solution' export default function SubmitForm({ problemId }: { problemId: string }) { return ( <form action={submitSolution}> <input type="hidden" name="problemId" value={problemId} /> <textarea name="code" placeholder="Твоє рішення..." rows={10} /> <button type="submit">Відправити рішення</button> </form> ) } // Форма відправляє на Server Action, записує в БД, оновлює кешовану сторінку задачі ```Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.