React Server Components у продакшені: патерни та пастки

React Server Components у продакшені: перевірені патерни, поширені антипатерни та стратегії налагодження для надійних застосунків Next.js 15.

Патерни та пастки React Server Components у продакшені

React Server Components (RSC) фундаментально змінюють принцип серверного рендерингу в Next.js 15, проте впровадження у продакшені виявляє пастки, які офіційна документація не завжди висвітлює. Ця стаття розбирає патерни, які працюють, ті, що ламаються, і способи діагностувати проблеми, перш ніж вони потраплять у продакшен.

Server Components і Client Components

Server Component виконується виключно на сервері та надсилає нуль JavaScript у браузер. Client Component (позначений директивою "use client") виконується з обох боків. Правило: тримати Client Components якомога меншими і якомога нижче в дереві.

Межа сервер-клієнт: розуміння патерну boundary

Найпоширеніша пастка RSC стосується межі між Server і Client Components. Як тільки компонент має директиву "use client", усі його імпортовані діти теж стають Client Components, навіть без директиви.

ProductPage.tsx (Server Component)tsx
import { ProductDetails } from './ProductDetails'
import { AddToCartButton } from './AddToCartButton'

export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const product = await getProduct(id)

  return (
    <div>
      {/* Server Component: прямий доступ до БД */}
      <ProductDetails product={product} />
      {/* Client Component: ізольована інтерактивність */}
      <AddToCartButton productId={product.id} price={product.price} />
    </div>
  )
}
AddToCartButton.tsx (Client Component)tsx
'use client'

import { useState } from 'react'

export function AddToCartButton({ productId, price }: { productId: string; price: number }) {
  const [adding, setAdding] = useState(false)

  async function handleAdd() {
    setAdding(true)
    await fetch('/api/cart', {
      method: 'POST',
      body: JSON.stringify({ productId, quantity: 1 }),
    })
    setAdding(false)
  }

  return (
    <button onClick={handleAdd} disabled={adding}>
      {adding ? 'Додавання...' : `Додати в кошик — $${price}`}
    </button>
  )
}

Ключовий патерн: передавати дані як серіалізовані пропси від Server Component до Client Component. Функції, класи та об'єкти Date не можуть перетнути цю межу.

Антипатерн: зайвий обгортковий Client Component

Частою помилкою є створення Client Component, який обгортає Server Component-дітей, змушуючи усе піддерево перейти на бік клієнта.

PageWrapper.tsx — АНТИПАТЕРНtsx
'use client'

import { useState } from 'react'

// Увесь дочірній вміст переходить на бік клієнта
export function PageWrapper({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState('light')
  return (
    <div className={theme}>
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        Змінити тему
      </button>
      {children}
    </div>
  )
}
children як слот

Розв'язок: передавати Server Components як children (патерн слот). Діти, передані як пропси, залишаються Server Components, навіть коли батько є Client Component. Наведений код працює коректно, доки children приходить від батька-Server Component.

layout.tsx (Server Component)tsx
import { PageWrapper } from './PageWrapper'
import { HeavyServerContent } from './HeavyServerContent'

export default function Layout() {
  return (
    <PageWrapper>
      {/* Залишається Server Component попри клієнтський обгортковий */}
      <HeavyServerContent />
    </PageWrapper>
  )
}

Цей патерн композиції зберігає переваги серверного рендерингу для важкого вмісту і водночас вмикає інтерактивність на рівні обгортки.

Обробка асинхронних даних: патерн fetch у компоненті

React 19 та Next.js 15 підтримують async/await безпосередньо в Server Components. Цей патерн спрощує отримання даних порівняно зі старішим підходом getServerSideProps.

UserProfile.tsx (Server Component)tsx
import { cache } from 'react'

// Дедуплікує однакові виклики в межах одного рендеру
const getUser = cache(async (userId: string) => {
  const res = await fetch(`https://api.example.com/users/${userId}`, {
    next: { revalidate: 3600 }, // Кеш на 1 годину
  })
  if (!res.ok) throw new Error('User not found')
  return res.json()
})

export default async function UserProfile({ userId }: { userId: string }) {
  const user = await getUser(userId)

  return (
    <section>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <p>Учасник з {new Date(user.createdAt).toLocaleDateString('uk-UA')}</p>
    </section>
  )
}

Три критичні моменти:

  • Функція cache() React дедуплікує однакові виклики під час одного серверного рендеру
  • next: { revalidate } керує тривалістю кешу на боці Next.js
  • Помилки в асинхронному Server Component активують найближчий error.tsx

Пастка серіалізації: що не перетинає межу

Дані, що передаються між Server і Client Components, мають бути серіалізованими у JSON. Ось що спричиняє тихі помилки або падіння.

tsx
// ПАСТКА: передавання несеріалізованих типів
// Функція — не працює
<ClientComp onSubmit={async (data) => { /* server action */ }} />
// Натомість використати імпортовану Server Action
import { submitForm } from '@/lib/actions/form'
<ClientComp onSubmit={submitForm} />

// Об'єкт Date — не працює
<ClientComp createdAt={new Date()} />
// ISO-рядок — працює
<ClientComp createdAt={new Date().toISOString()} />

// Map, Set, RegExp — не працює
<ClientComp data={new Map([['key', 'value']])} />
// Звичайний об'єкт або масив — працює
<ClientComp data={{ key: 'value' }} />

Server Actions (функції з директивою "use server") є винятком: їх можна передавати як пропси Client Component, тому що Next.js перетворює їх на HTTP-ендпоінти.

Готовий до співбесід з React / Next.js?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Streaming і Suspense: патерни поступового завантаження

SSR-стрімінг із Suspense надсилає HTML у браузер поступово. Оптимальний патерн використовує гранулярні межі Suspense навколо кожної асинхронної секції.

DashboardPage.tsx (Server Component)tsx
import { Suspense } from 'react'
import { RevenueChart } from './RevenueChart'
import { RecentOrders } from './RecentOrders'
import { UserStats } from './UserStats'

export default function DashboardPage() {
  return (
    <div className="grid grid-cols-2 gap-6">
      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart />
      </Suspense>

      <Suspense fallback={<StatsSkeleton />}>
        <UserStats />
      </Suspense>

      <Suspense fallback={<TableSkeleton />}>
        <RecentOrders />
      </Suspense>
    </div>
  )
}

Кожна секція завантажується незалежно. Якщо RevenueChart займає 3 секунди, а UserStats — 200 мс, статистика з'являється миттєво, не чекаючи графіка.

Suspense та SEO

Вміст усередині межі Suspense рендериться на сервері та потрапляє в початковий HTML. Краулери бачать повний вміст. Стрімінг впливає лише на швидкість доставки в браузер, а не на видимість для SEO.

Налагодження у продакшені: відстеження проблем RSC

Помилки RSC часто є криптичними. Три діагностичні техніки працюють у продакшені.

1. Виявлення невідповідностей гідрації

debug-hydration.tsxtsx
'use client'

import { useEffect, useState } from 'react'

export function HydrationDebug() {
  const [isClient, setIsClient] = useState(false)

  useEffect(() => {
    setIsClient(true)
  }, [])

  if (process.env.NODE_ENV !== 'development') return null

  return (
    <div style={{ position: 'fixed', bottom: 0, right: 0, padding: '4px 8px', fontSize: 12 }}>
      {isClient ? 'Client' : 'Server'}
    </div>
  )
}

2. Логування RSC-payload

У Next.js 15 увімкнути логування RSC у next.config.ts:

next.config.tstypescript
const nextConfig = {
  logging: {
    fetches: {
      fullUrl: true, // Показує повні URL fetch
    },
  },
}

export default nextConfig

3. Перевірка розміру payload

Занадто великий RSC-payload (> 128 КБ) погіршує продуктивність. Слід відстежувати мережеві запити з content type text/x-component у DevTools.

Просунутий патерн: композиція з Server Actions

Server Actions у поєднанні зі Server Components створюють природний патерн CQRS: читання на сервері (RSC), записи через actions.

TodoList.tsx (Server Component)tsx
import { getTodos } from '@/lib/services/todo'
import { TodoForm } from './TodoForm'
import { deleteTodo } from '@/lib/actions/todo'

export default async function TodoList() {
  const todos = await getTodos()

  return (
    <div>
      <TodoForm />
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            {todo.title}
            <form action={deleteTodo}>
              <input type="hidden" name="id" value={todo.id} />
              <button type="submit">Видалити</button>
            </form>
          </li>
        ))}
      </ul>
    </div>
  )
}
actions/todo.tstsx
'use server'

import { revalidatePath } from 'next/cache'
import { TodoService } from '@/lib/services/todo'

export async function deleteTodo(formData: FormData) {
  const id = formData.get('id') as string
  await TodoService.delete(id)
  revalidatePath('/todos')
}

Виклик revalidatePath запускає свіжий рендер Server Component з оновленими даними без повного перезавантаження сторінки.

Для глибшої підготовки до співбесіди з цих тем варто звернутись до модуля Next.js Server Actions та модуля Next.js Data Fetching на SharpSkill. Офіційна документація React описує повну специфікацію Server Components.

Висновок

  • Тримати Client Components малими та ізольованими в нижній частині дерева компонентів
  • Використовувати патерн слот (children), щоб зберегти Server Components усередині клієнтського обгорткового
  • Завжди перевіряти серіалізованість пропсів, що перетинають межу сервер-клієнт
  • Розміщувати гранулярні межі Suspense навколо кожної незалежної асинхронної секції
  • Стежити за розміром RSC-payload у продакшені (ціль < 128 КБ)
  • Поєднувати Server Components (читання) і Server Actions (записи) для природного патерну CQRS
  • Використовувати cache() React для дедуплікації запитів у межах одного серверного рендеру

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Теги

#react server components
#next.js 15
#rsc patterns
#production
#react 19

Поділитися

Пов'язані статті