Modern Next.js Mapped: Що замінює кожна функція та коли її використовувати
Екскурсія, присвятована восьми функціям Next.js — від Server Components до Metadata API, яка показує, які старі підходи кожна з них замінює та де вони можуть спричинити проблеми.
Next.js тепер керує маршрутизацією, отриманням даних, кешуванням, стратегією відображення та багатьма аспектами вашого API. Проте команди часто впроваджують його, але зберігають старі звички: отримання даних у функції useEffect, ручна налаштування API-маршрутів для кожної форми, правила кешування, які ніхто не може пояснити. Цей посібник охоплює вісім функціоналів, старі підходи, які вони замінюють, та деталі, які спричиняють проблеми у реальних проектах, щоб ви могли окремо вирішувати, що слід включити до вашого кодбазу.
Чому фреймворк тепер охоплює весь стек
Walmart, Nike, TikTok, OpenAI та Airbnb використовують Next.js для своїх веб-додатків, і перевага полягає у консолідації: маршрутизація, пакетування, режими відображення, доступ до даних та серверні кінцеві точки мають один репозиторій та один набір конвенцій. Вибір маршрутизатора та налаштування пакетувальника більше не є частиною старту проекту, тож цей час витрачається на розробку продукту.
1. Компоненти сервера роблять сервер стандартним місцем для відображення
React Server Components (RSC) — це найбільша архітектурна зміна у цьому списку. Компонент сервера виконується лише на сервері та надсилає отриманий результат у браузер, тому його код ніколи не стає частиною клієнтського пакету JavaScript.
Що зникає зі сторінки, що базується на даних
У класичному React з відображенням на клієнті навіть компонент, який лише читає дані та виводить список, додає свій код, логіку отримання даних та залежності до пакету, а потім ініціюється у браузері. За допомогою RSC компонент читає дані на сервері та надсилає лише результат. У App Router кожен компонент є компонентом сервера, якщо ви не вказали інше, тому наведений нижче файл зовсім не потребує жодних директив:
// app/products/page.tsx
// This component runs ONLY on the server. No "use client" directive needed.
Сторінка є async функцією, яка безпосередньо запитує базу даних та повертає маркап. Між нею немає API-шляху та жодних запитів з боку клієнта:
import { db } from "@/lib/db";export default async function ProductsPage() {
// Direct database access. No API route. No fetch boilerplate.
const products = await db.product.findMany({ take: 20 }); return (
<main>
<h1>Our Products</h1>
<ul>
{products.map((product) => (
<li key={product.id}>
<h2>{product.name}</h2>
<p>${product.price}</p>
</li>
))}
</ul>
</main>
);
}
Немає useState, немає useEffect, немає прапорця завантаження, немає fetch до власного бекенду. У браузері виконується менше коду, HTML надходить повністю готовим (що корисно для пошукових систем), і його легше підтримувати. Оскільки цей код безпосередньо взаємодіє з базою даних, ніколи не імпортуйте його до файлу клієнта; модуль даних, призначений лише для сервера, чітко визначає цей межу.
Де все одно потрібні компоненти клієнта
Вам все ще потрібні компоненти клієнта для будь-яких інтерактивних елементів: обробників подій, стану, ефектів та API браузера, таких як localStorage. Такий файл позначається шляхом розміщення директиви "use client" у верхній частині:
// components/AddToCartButton.tsx
"use client";
Кнопка нижче зберігає частину локального стану та реагує на кліки, що саме є типом операцій, які мають виконуватися у браузері:
import { useState } from "react";export function AddToCartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false); return (
<button onClick={() => setAdded(true)}>
{added ? "Added!" : "Add to Cart"}
</button>
);
}
Почніть з серверних компонентів та переходьте на клієнтські лише там, де це необхідно для інтерактивності, використовуючи "use client" якомога нижче в ієрархії: сторінка серверу, яка містить одну невелику клієнтську кнопку, передає значно менше JavaScriptу, ніж сторінка, яка є клієнтським компонентом з самого початку. Щоб детальніше дізнатися про те, як працює модель відображення, перегляньте архітектуру безкоштовного відображення.
2. Дії сервера замінюють маршрути API для змін даних
Server Actions дозволяють вам написати функцію, яка виконується на сервері, та викликати її безпосередньо з компонента. Next.js генерує HTTP-кінцеву точку, серіалізує аргументи та повертає результат, тож вам більше не потрібно підтримувати окремий маршрут лише для обробки форми.
Схема з двома файлами, яку він відміняє
Раніше для обробки змін потрібен був обробник у папці api Pages Router, який читав тіло запиту, зберігав дані в базі даних та відповідав у форматі JSON:
// You needed an API route
// pages/api/create-post.ts
export default async function handler(req, res) {
const { title, content } = req.body;
await db.post.create({ data: { title, content } });
res.status(200).json({ success: true });
}
Потім компонент мусив вручну викликати цю кінцеву точку, серіалізуючи сам вміст запиту:
// Then in your component:
const response = await fetch("/api/create-post", {
method: "POST",
body: JSON.stringify({ title, content }),
});
Перевірена дія, розміщена поруч із формою
За допомогою Server Actions сторінка імпортує все необхідне, включаючи Zod для перевірки введених даних:
// app/posts/new/page.tsx
import { redirect } from "next/navigation";
import { db } from "@/lib/db";
import { z } from "zod";
Дія оголошується поруч із формою. Директива "use server" всередині тіла функції позначає її як код, призначений лише для сервера; форма передає її до свого атрибута action. Ця дія перевіряє об’єкт FormData за допомогою функції safeParse, повертає помилки полів у разі невдачі перевірки, а в іншому випадку записує дані та перенаправляє:
const schema = z.object({
title: z.string().min(3, "Title must be at least 3 characters"),
content: z.string().min(10, "Content is too short"),
});async function createPost(formData: FormData) {
"use server"; const parsed = schema.safeParse({
title: formData.get("title"),
content: formData.get("content"),
}); if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors };
} await db.post.create({ data: parsed.data });
redirect("/posts");
}export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Post title" required />
<textarea name="content" placeholder="Write something..." required />
<button type="submit">Publish</button>
</form>
);
}
Перевірка є обов’язковою, оскільки Server Action — це публічний кінцевий пункт, який будь-хто може викликати з довільними даними. З тієї ж причини перевірки авторизації мають знаходитися безпосередньо всередині самої дії; чому Server Actions потребують перевірки авторизації всередині кожного тіла функції детально пояснює це. Також зверніть увагу, що тут ніщо не читає повернений об’єкт помилок, тому при невдачі перевірки нічого не відображається. Наступний паттерн вирішує цю проблему.
Відображення помилок та статусу очікування за допомогою useActionState
Коли форма має відображати повідомлення про перевірку чи призупиняти кнопку під час обробки запиту, перемістіть форму до клієнтського компонента та обгорніть дію за допомогою гука React useActionState:
"use client";
Гук повертає останній стан, створений дією, обгорнену функцію formAction для передачі формі, а також флаг isPending. Компонент відображає першу помилку для кожного поля та змінює текст кнопки під час обробки даних:
import { useActionState } from "react";
import { createPost } from "./actions";export function PostForm() {
const [state, formAction, isPending] = useActionState(createPost, null); return (
<form action={formAction}>
<input name="title" placeholder="Post title" />
{state?.error?.title && (
<p className="text-red-500">{state.error.title[0]}</p>
)}
<textarea name="content" placeholder="Write something..." />
{state?.error?.content && (
<p className="text-red-500">{state.error.content[0]}</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? "Publishing..." : "Publish"}
</button>
</form>
);
}
Деталь, яка часто спричиняє плутанину: коли дію використовують через useActionState, React передає їй попередній стан як перший аргумент, а FormData — як другий. Функція createPost, показана раніше, приймає лише formData, тому версія, експортована з ./actions для цього хука, мусить мати сигнатуру (prevState, formData). Крім того, дія має знаходитися у окремому файлі з позначкою "use server" у верхній частині, оскільки клієнтський компонент не може визначати функції сервера безпосередньо в коді.
3. Turbopack скорочує цикл отримання зворотного зв’язку під час розробки
Протягом тривалого часу webpack визначав темпи локальної розробки. Функція Fast Refresh була зручною після запуску сервера, але початок роботи великих додатків міг займати від 30 до 60 секунд. Turbopack — це бандлер на основі Rust від Vercel, створений для усунення цього бар’єру.
Turbopack досяг 100% рівня проходження усіх 8 298 тестів інтеграції з Next.js, а команди повідомляють про час початку роботи додатків менше 3 секунд у проектах, яким раніше потрібен був понад хвилину. Його рівень зрілості швидко змінюється між версіями, тому перевіряйте актуальну документацію щодо його статусу у вашій версії, особливо щодо продакшн-будов.
Увімкнення
Щоб увімкнути цю функцію під час розробки, достатньо вказати один параметр у скрипті dev:
// package.json
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build"
}
}
Не потрібна додаткова налаштування. Turbopack працює інкрементально, перебудовуючи лише те, що змінилося, тому він найбільше корисний для великих проєктів. Якщо ви використовуєте власні завантажувачі чи плагіни webpack, спочатку переконайтеся у наявності еквівалентів у Turbopack. Наше порівняння бандлерів розглядає ці аспекти.
4. Часткове попереднє генерування поєднує статичну оболонку з потоковими даними
Часткове попереднє генерування (PPR) негайно передає попередньо створену статичну HTML-оболонку та потоково додає до неї динамічні частини тієї самої сторінки, усе це в межах одної відповіді.
На сторінці продукту макет, навігація та опис є однаковими для всіх; особистізована ціна, кошик та кількість запасів — ні. PPR одразу надсилає оболонку та доповнює її елементами за потреби під час обробки запиту.
Сторінка імпортує один статичний компонент та два динамічні:
// app/product/[id]/page.tsx
import { Suspense } from "react";
import { ProductDetails } from "./ProductDetails"; // static
import { PersonalizedPrice } from "./PersonalizedPrice"; // dynamic
import { StockStatus } from "./StockStatus"; // dynamic
Межа між статичним та динамічним визначається за допомогою Suspense. Усе, що знаходиться за межами цієї межі, може бути попередньо відрендеровано; кожен елемент Suspense стає місцем заміни в шеллі, яке замінюється після того, як його дочірні елементи завершать відрендерування на сервері:
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<div>
{/* This renders statically - instant */}
<ProductDetails id={params.id} /> {/* These stream in dynamically */}
<Suspense fallback={<div>Loading price...</div>}>
<PersonalizedPrice productId={params.id} />
</Suspense> <Suspense fallback={<div>Checking stock...</div>}>
<StockStatus productId={params.id} />
</Suspense>
</div>
);
}
Зауважте, що params тут оголошується як звичайний об’єкт. У новіших версіях Next.js params передається у вигляді Promise та потребує очікування, тому необхідно скоригувати його сигнатуру відповідно до версії, яку ви використовуєте.
PPR було введено за допомогою експериментального флага в конфігурації Next.js, починаючи з імпорту типу:
// next.config.ts
import type { NextConfig } from "next";
а потім сам флаг:
const nextConfig: NextConfig = {
experimental: {
ppr: true,
},
};export default nextConfig;
На момент написання цього тексту цей флаг було переорганізовано у новіших версіях (поведінка PPR пов’язана з налаштуванням Cache Components у Next.js 16), тому сприймайте цей фрагмент як ілюстративний та перевірте поточну назву опції. У будь-якому разі сторінка здається статичною, оскільки її шаблон зберігається у кеші, тоді як дані залишаються актуальними. Механізми цього ми детальніше розглядаємо у статті про часткове попереднє формування та одночасне формування.
5. Використання директиви use cache робить кешування явним
Next.js 13 та 14 за замовчуванням активно кешували контент, і багато команд виявляли у продакшені застарілі сторінки без очевидних причин. Next.js 16 переходить до опційного кешування за допомогою Cache Components та директиви use cache: ви самі вказуєте, що потрібно зберігати у кеші, замість того щоб здогадуватися, що вже там є.
Розміщення цієї директиви у верхній частині асинхронного компонента дозволяє зберігати у кеші його відображений результат:
// A component that caches its output for 1 hour
async function PopularArticles() {
"use cache";
Решта компонента отримує дані та відображає їх як зазвичай:
const articles = await fetch("https://api.example.com/popular-articles").then(
(r) => r.json()
); return (
<ul>
{articles.map((article: { id: string; title: string }) => (
<li key={article.id}>{article.title}</li>
))}
</ul>
);
}
У примітці згадується одна година, але сама директива не встановлює термін дії; цей термін визначається профілем кешу, який застосовується за допомогою cacheLife, а в іншому випадку використовується стандартний профіль. Власні профілі оголошуються у конфігурації:
// next.config.ts
const nextConfig = {
experimental: {
cacheLife: {
"stale-for-a-day": {
stale: 60 * 60, // 1 hour
revalidate: 60 * 60 * 24, // 1 day
expire: 60 * 60 * 24 * 7, // 1 week
},
},
},
};
Ці три значення відповідають на різні запитання. stale — це термін, протягом якого клієнт може використовувати свою копію даних, не перевіряючи їх на сервері; revalidate — це частота, з якою сервер оновлює запис у фоновому режимі; expire — це момент, після якого запис видаляється, і наступний запит змушений чекати на свіжі дані. Те, чи належить cacheLife до категорії experimental, залежить від вашої версії. Щодо анулювання даних на основі тегів, яке знадобиться після зміни даних під час запису, дивіться наш посібник з використання кешу та анулювання даних за допомогою тегів.
6. Функції потокового AI з використанням AI SDK
Vercel AI SDK інтегрується з Route Handlers та React hooks, тож потокові чати, пошук за допомогою AI та генеровані інтерфейси користувача стають звичайним кодом додатку.
На сервері обробник маршруту імпортує streamText та постачальника моделей:
// app/api/chat/route.ts
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
Обробник POST читає розмову з тіла запиту, починає стрімінгове виконання та повертає його у вигляді стрімованої відповіді (закриваюча дужка функції в уривку відсутня):
export async function POST(req: Request) {
const { messages } = await req.json(); const result = streamText({
model: openai("gpt-4o"),
messages,
}); return result.toDataStreamResponse();
На клієнті сторінка чату є компонентом клієнта, оскільки вона зберігає стан введення:
// app/chat/page.tsx
"use client";
Хук useChat керує списком повідомлень, значенням введення та його надсиланням, а також перерисовується при надходженні нових токенів:
import { useChat } from "ai/react";export default function ChatPage() {
const { messages, input, handleInputChange, handleSubmit } = useChat(); return (
<div>
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} placeholder="Ask anything..." />
<button type="submit">Send</button>
</form>
</div>
);
}
Це приблизно 30 рядків для стрімингового чату, з обробником маршрутів на бекенді та useChat на фронтенді. Пам’ятайте, що API AI SDK швидко змінюється: у новіших версіях хук імпортується з @ai-sdk/react, стан вхідних даних керується вами, а помічник для обробки відповідей має іншу назву. Збережіть потрібні версії та ознайомтесь з документацією SDK перед копіюванням цього коду. Щодо інтерфейсів у стилі агента з інструментами та кількома кроками, дивіться створення UI для багатокрокових AI-агентів за допомогою Next.js та AI SDK.
7. Шаблони App Router для складних макетів
App Router, представлений у Next.js 13, тепер є стандартним способом створення додатків на Next.js. Два з його функціоналів замінюють потребу у власному керуванні станом.
Паралельні маршрути для незалежних панелей дашборду
Паралельні маршрути відображають кілька сторінок у одному лейауті одночасно. Кожна папка, попереджена символом @, визначає іменовану щілину:
app/
dashboard/
@analytics/
page.tsx
@recent/
page.tsx
layout.tsx
page.tsx
Лейаут отримує кожну щілину як параметр разом із children та розміщує їх у сітці:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
recent,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
recent: React.ReactNode;
}) {
return (
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2">{children}</div>
<aside>
{analytics}
{recent}
</aside>
</div>
);
}
Оскільки кожна щілина є окремим сегментом маршруту, вона завантажує власні дані та може мати власні стани завантаження та помилок. Повільний запит до аналітики не впливає на панель останньої активності. Одна з проблем: коли ви переходите до підшляху, який не визначений жодною щілиною, Next.js потребує файлу default.tsx у цій щілині, щоб знати, що відображати під час повторного завантаження.
Перехоплення маршрутів для модалок з реальними URL
Поширений шаблон інтерфейсу відкриває елемент у модальному вікні, поки URL змінюється на адресу цього елемента, щоб його можна було поділитися. Обробка маршрутів здійснюється за допомогою конвенцій папок:
app/
photos/
[id]/
page.tsx // Full page view at /photos/123
(..)[id]/
page.tsx // Intercepted modal view
page.tsx
Коли користувач клацає у мережці, відповідна папка перехоплює навігацію з боку клієнта та відображає версію у модальному вікні. Коли хтось відкриває URL безпосередньо або оновлює сторінку, замість цього відображається звичайна повна сторінка, без будь-яких ручних трюків з історією переходів. Маркери (.), (..) та (...) відносяться до сегментів маршруту, а не до папок файлової системи, і модальне вікно зазвичай відображається через паралельний слот @modal, тому перевірте документацію до маршрутизації, щоб дізнатися про точний формат, необхідний для вашої структури.
8. Основи SEO: метадані та зображення
API Metadata перетворює SEO на звичайний код, який знаходиться поруч із сторінкою, яку він описує. Після імпорту типу Metadata:
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
сторінка експортує функцію generateMetadata, яка завантажує запис та повертає заголовок, опис, дані Open Graph та картку Twitter:
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata> {
const post = await getPost(params.slug); return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.coverImage }],
type: "article",
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
};
}
Попередні перегляди у соцмережах, канонічні URL та структуровані дані походять з тих самих даних, які відображає сторінка. Якщо компонент сторінки також викликає getPost, необхідно усунути дублювання запитів (наприклад, за допомогою cache у React), щоб база даних не оброблялася двічі.
Для зображень варто використовувати next/image як стандартний варіант:
import Image from "next/image";
За замовчуванням він використовує лазі-лоадинг, подає варіанти правильного розміру та конвертує зображення у формати WebP або AVIF, якщо браузер їх підтримує. Вказані значення width та height також забезпечують вільний простір та запобігають зміщенню макету:
export function ProductCard({ product }: { product: Product }) {
return (
<div>
<Image
src={product.imageUrl}
alt={product.name}
width={400}
height={300}
priority={false} // set true for above-the-fold images
/>
<h2>{product.name}</h2>
</div>
);
}
Встановіть priority на true лише для зображення, яке видно вгорі сторінки, зазвичай головного зображення або зображення продукту, щоб браузер завантажив його раніше.
Розумний порядок навчання
Кожен крок ґрунтується на попередньому:
- Стандарти назв файлів у App Router:
layout.tsx,page.tsx,loading.tsxтаerror.tsx. - Серверні компоненти та місце розташування користувацького інтерфейсу.
- Дії на сервері з валідацією за допомогою Zod для кожної зміни.
- Функції Suspense та стрімінг для описового керування станами завантаження.
Основні висновки
- Вважайте сервер стандартним середовищем виконання та передавайте параметр
"use client"до найменших інтерактивних елементів. - Дії сервера є кінцевими точками: у кожній з них перевіряйте вхідні дані та права доступу.
- Визначайте межі статичного та динамічного контенту за допомогою
Suspense; PPR та кешування будуються на цих межах. - Віддавайте перевагу явному кешуванню за допомогою
use cacheта профілівcacheLifeз конкретними назвами замість використання стандартних налаштувань. - Багато з цих API змінилися між останніми версіями, тому перевіряйте прапорці та сигнатури відповідно до версії, яку ви фактично використовуєте.
Незалежно від того, чи починаєте ви з нуля, чи мігруєте великий додаток до App Router, поступове впровадження цих елементів є способом з мінімальним ризиком.
Пов’язана література
- 20 передових шаблонів Next.js 16 для архітектури додатків високого рівня — огляд підходу сервер-на-першому місці, кешування, стрімінгу, PPR, паралельних та перехоплюючих маршрутів, а також інших шаблонів для створення масштабованих додатків на Next.js 16.
- Розуміння компонентів кешу та часткового попереднього завантаження в Next.js 16.3 — пояснює, як функція миттєвого навігування в Next.js 16.3 використовує спільні оболонки маршрутів та чіткі рішення щодо стрімінгу, щоб зробити додатки, генеровані на сервері, миттєвими у використанні.