Де провести межу сервер-клієнт у сторінці Next.js App Router
Практична ментальна модель для React Server Components: що виконується де, як сторінка блогової публікації ділиться на серверну та клієнтську частини, а також правила імпорту, яких слід дотримуватися.
Компоненти сервера React часто здаються складними навіть після прочитання документації, головним чином тому, що вони змушують відмовитися від припущення, яке кожен розробник React мав протягом багатьох років: а саме, що компоненти працюють у браузері. Як тільки це припущення зникає, решта стає досить логічною. У цій статті будується ментальна модель навколо однієї сторінки блогу, де показано, які частини мають знаходитися на сервері, які — на клієнті, а також кілька правил, які забезпечують чітке розділення між ними.
Щоб детальніше дізнатися про сам процес відображення, перегляньте архітектуру безпакетного відображення за допомогою серверних компонентів.
Від „усе гідратується“ до „лише те, що потрібно“
До появи серверних компонентів кожен компонент React у підсумку виконувався у браузері. Навіть із серверною обробкою відображення весь JavaScript для всього дерева компонентів надсилався на клієнт та ініціалізувався, щоб додаток міг стати інтерактивним. Це є марною тратою ресурсів для компонентів, які лише отримують дані та передають їх як параметри: їхній код завантажується та виконується, не реагуючи на дії користувача.
Серверний компонент змінює ситуацію, оскільки виконується лише на сервері. Його код ніколи не надсилається у браузер та не ініціалізується, тому він не додає жодного JavaScript до пакету клієнта. У браузер надходить лише оброблений результат його роботи, який React серіалізує у компактний пакет разом із HTML.
Найпростіший спосіб розрізнити ці два типи:
- Клієнтський компонент: виконується у браузері (після попередньої обробки на сервері), може зберігати стан та реагувати на події.
Чому це розрізнення має значення
Візьмемо типову сторінку блогової публікації. Заголовок та текст походять з бази даних, виглядають однаково для кожного відвідувача та ігнорують кліки. Традиційно код, який їх відображає, разом із будь-якими бібліотеками Markdown чи форматування, все одно надсилається до браузера. За допомогою компонентів сервера все це залишається на сервері. Розмір пакету зменшується, а сторінки завантажуються швидше, особливо на повільніших пристроях.
Команди, які переходять на App Router, побудований навколо компонентів сервера, повідомляють про кращі показники Core Web Vitals у деяких сценаріях, зокрема LCP. Вважайте це залежним від контексту: користь залежить від того, скільки клієнтського JavaScript ви усуваєте зі своїх сторінок, тож вимірюйте показники для своїх маршрутів.
Сторінка блогової публікації, розділена на дві частини
У проекті Next.js App Router файл сторінки є серверною складовою, якщо не зазначено інше. Наведена нижче сторінка — це async функція, яка очікує дані безпосередньо з рівня даних, обробляє ситуацію їх відсутності, відображає статичний контент, а потім додає одну інтерактивну дочірню складову для коментарів. Вона написана з урахуванням API Next.js 14:
// app/posts/[slug]/page.tsx
// This is a Server Component by default — no "use client" needed
import { getPostBySlug } from "@/lib/db"
import { PostContent } from "@/components/PostContent"
import { CommentSection } from "@/components/CommentSection"
type Props = {
params: { slug: string }
}
export default async function PostPage({ params }: Props) {
// Direct DB call — no useEffect, no API route, no loading state
const post = await getPostBySlug(params.slug)
if (!post) {
return <div>Post not found.</div>
}
return (
<article className="max-w-2xl mx-auto py-12 px-4">
<h1 className="text-3xl font-bold mb-4">{post.title}</h1>
<PostContent content={post.body} />
{/* This one needs interactivity — so it's a Client Component */}
<CommentSection postId={post.id} />
</article>
)
}
Зверніть увагу на те, чого немає: жодного useState, жодного useEffect, жодної обгортки API-шляху та жодного відстеження стану завантаження. Дані очікуються точно так само, як у будь-якій іншій асинхронній функції, що є основою цієї моделі.
Дві практичні примітки. Починаючи з Next.js 15, params передається у вигляді Promise, тому сторінка має спочатку виконати await params, перш ніж читати slug; перевірте версію, яку ви використовуєте. А щодо справжнього випадку незнайдення сторінки, виклик notFound() з модуля next/navigation повертає справжній статус 404, а не звичайну сторінку з повідомленням про помилку.
Блок для коментарів відрізняється. Він зберігає текст чернетки у стані та реагує на набір тексту та кліки, тому мусить працювати у браузері. Директива "use client" у верхній частині файлу позначає його як клієнтський компонент:
// components/CommentSection.tsx
"use client" // opts into browser rendering
import { useState } from "react"
type Props = {
postId: string
}
export function CommentSection({ postId }: Props) {
const [comment, setComment] = useState("")
const handleSubmit = async () => {
await fetch("/api/comments", {
method: "POST",
body: JSON.stringify({ postId, comment }),
})
setComment("")
}
return (
<div className="mt-8">
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Leave a comment..."
className="w-full border rounded p-2 text-sm"
/>
<button
onClick={handleSubmit}
className="mt-2 bg-blue-600 text-white px-4 py-2 rounded text-sm"
>
Post Comment
</button>
</div>
)
}
Усе інтерактивне знаходиться тут: локальний стан для текстового поля, обробник змін та обробник надсилання даних, який відправляє їх на маршрут API, а потім очищує поле. Під час реальної налаштування до запиту слід додати заголовок Content-Type: application/json, а як альтернативу окремому маршруту API варто розглянути використання Server Action.
Отриманий розподіл простий для розуміння: сервер керує даними, а клієнт — інтеракцією.
Правила, які зберігають чистоту меж
- У App Router компоненти за замовчуванням є Server Components.
- Додавайте
"use client"лише там, де вам потрібні стани, ефекти, обробники подій чи API браузера, такі якwindowтаlocalStorage. - Server Components можуть імпортувати та відображати Client Components, як це робить сторінка з
CommentSection.
children або інші параметри, що дозволяє розміщувати вміст, отриманий на сервері, всередині інтерактивної оболонки."use client" також позначає межу, а не окремий файл: все, що імпортується цим файлом, стає частиною клієнтського пакету. Розміщення цієї директиви якомога нижче в ієрархії, на невеликих інтерактивних елементах, дозволяє залишити решту на сервері.
Підсумок
Ментальна модель стає зрозумілою, як тільки перше запитання до компонента — «де це виконується?» — отримує відповідь «на сервері», що є стандартною відповіддю App Router.
- Компоненти сервера переміщують процес відображення та доступ до даних на сервер, і не постачають жодного JavaScript-коду компонента.
- Завантаження даних стає звичайним використанням
async/awaitвсередині компонента. - Розглядайте
"use client"як опцію для інтерактивних елементів, а не як стандарт. - Практичний перший крок: виберіть один компонент для завантаження даних у існуючому проекті, перевірте, чи справді він потребує браузера, та перетворіть його, якщо ні.
З такою моделлю лейаути, технологія Suspense для стрімінгу даних, Server Actions та паралельні маршрути стають набагато простішими для опанування, оскільки кожен з них ґрунтується на тій самій основі, що робить сервер пріоритетним.
Пов’язана література
- Створення стійкої сторінки деталей фільму за допомогою Next.js App Router — Дізнайтеся, як правильно отримувати та кешувати дані API OMDB у Next.js App Router за допомогою асинхронних Server Components, параметрів await та ефективного оброблення помилок 404.
- Server Actions чи Route Handlers? Посібник з прийняття рішень для Next.js 16 — Дізнайтеся, коли мутація у Next.js 16 слід реалізовувати як Server Action, а коли їй потрібен Route Handler, з виправленими прикладами для форм, помилок, оптимізму та webhooks.