Головна / Статті / Де провести межу сервер-клієнт у сторінці Next.js App Router

Де провести межу сервер-клієнт у сторінці Next.js App Router

Практична ментальна модель для React Server Components: що виконується де, як сторінка блогової публікації ділиться на серверну та клієнтську частини, а також правила імпорту, яких слід дотримуватися.

1321 слів

Компоненти сервера 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 або інші параметри, що дозволяє розміщувати вміст, отриманий на сервері, всередині інтерактивної оболонки.
  • Параметри, які передаються від сервера до клієнта, мають бути серіалізованими: прості дані підходять, а функції та екземпляри класів — ні.
  • Стилювання не змінюється. Класи Tailwind та CSS працюють однаково в обох типах компонентів.
  • "use client" також позначає межу, а не окремий файл: все, що імпортується цим файлом, стає частиною клієнтського пакету. Розміщення цієї директиви якомога нижче в ієрархії, на невеликих інтерактивних елементах, дозволяє залишити решту на сервері.

    Підсумок

    Ментальна модель стає зрозумілою, як тільки перше запитання до компонента — «де це виконується?» — отримує відповідь «на сервері», що є стандартною відповіддю App Router.

    • Компоненти сервера переміщують процес відображення та доступ до даних на сервер, і не постачають жодного JavaScript-коду компонента.
    • Завантаження даних стає звичайним використанням async/await всередині компонента.
    • Розглядайте "use client" як опцію для інтерактивних елементів, а не як стандарт.
    • Практичний перший крок: виберіть один компонент для завантаження даних у існуючому проекті, перевірте, чи справді він потребує браузера, та перетворіть його, якщо ні.

    З такою моделлю лейаути, технологія Suspense для стрімінгу даних, Server Actions та паралельні маршрути стають набагато простішими для опанування, оскільки кожен з них ґрунтується на тій самій основі, що робить сервер пріоритетним.

    Пов’язана література

  • Міграція з next/router: Params, Shallow URLs та 404-сторінки в App Router — Дізнайтеся, як params, useParams, оновлення Shallow URLs, програмна навігація та справжні сторінки 404 функціонують у Next.js App Router після видалення next/router.
  • Технічний SEO у Next.js App Router: Metadata, ситемапи та JSON-LD — Дізнайтеся, як спільні інструменти для metadata, стандартний макет кореня, файл robots.ts, динамічні ситемапи, коректний JSON-LD та аудит сторінок створюють міцну основу для SEO у додатках Next.js.
  • Чому WebSockets застрягають у маршрутах API Next.js та як це виправляє користувацький сервер — Дізнайтеся, чому сервер ws усередині маршруту pages/api ніколи не завершує процедуру взаємодії, та як самостійно обробляти подію upgrade за допомогою користувацького сервера Next.js.