React Server Components na produkcji: wzorce i pułapki

React Server Components na produkcji: sprawdzone wzorce, częste antywzorce i strategie debugowania dla solidnych aplikacji Next.js 15.

Wzorce i pułapki React Server Components na produkcji

React Server Components (RSC) w fundamentalny sposób zmieniają działanie renderowania po stronie serwera w Next.js 15, ale wdrożenia produkcyjne ujawniają pułapki, których oficjalna dokumentacja nie zawsze opisuje. Ten artykuł rozkłada na czynniki pierwsze wzorce, które działają, te, które się rozpadają, oraz sposoby diagnozowania problemów, zanim trafią na produkcję.

Server Components a Client Components

Server Component działa wyłącznie na serwerze i wysyła zero JavaScriptu do przeglądarki. Client Component (oznaczony dyrektywą "use client") działa po obu stronach. Zasada: trzymać Client Components jak najmniejsze i jak najniżej w drzewie.

Granica server-client: zrozumienie wzorca boundary

Najczęstsza pułapka RSC dotyczy granicy między Server a Client Components. Gdy komponent ma dyrektywę "use client", wszystkie jego importowane dzieci również stają się Client Components, nawet bez tej dyrektywy.

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: bezpośredni dostęp do bazy */}
      <ProductDetails product={product} />
      {/* Client Component: izolowana interaktywność */}
      <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 ? 'Dodawanie...' : `Dodaj do koszyka — $${price}`}
    </button>
  )
}

Kluczowy wzorzec: przekazywać dane jako serializowalne propsy z Server Component do Client Component. Funkcje, klasy i obiekty Date nie mogą przekroczyć tej granicy.

Antywzorzec: zbędny wrapper Client Component

Częsty błąd to tworzenie Client Component, który opakowuje dzieci Server Components, wymuszając przeniesienie całego poddrzewa na stronę klienta.

PageWrapper.tsx — ANTYWZORZECtsx
'use client'

import { useState } from 'react'

// Cała zawartość dzieci trafia na stronę klienta
export function PageWrapper({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState('light')
  return (
    <div className={theme}>
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        Zmień motyw
      </button>
      {children}
    </div>
  )
}
children jako slot

Rozwiązanie: przekazać Server Components jako children (wzorzec slot). Dzieci przekazane jako propsy pozostają Server Components, nawet gdy rodzic jest Client Component. Powyższy kod działa poprawnie, dopóki children pochodzi z rodzica będącego Server Component.

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

export default function Layout() {
  return (
    <PageWrapper>
      {/* Pozostaje Server Component mimo wrappera klienta */}
      <HeavyServerContent />
    </PageWrapper>
  )
}

Ten wzorzec kompozycji zachowuje korzyści renderowania po stronie serwera dla ciężkiej treści, jednocześnie umożliwiając interaktywność na poziomie wrappera.

Obsługa danych asynchronicznych: wzorzec fetch w komponencie

React 19 i Next.js 15 obsługują async/await bezpośrednio w Server Components. Ten wzorzec upraszcza pobieranie danych w porównaniu ze starszym podejściem getServerSideProps.

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

// Deduplikuje identyczne wywołania w obrębie tego samego renderu
const getUser = cache(async (userId: string) => {
  const res = await fetch(`https://api.example.com/users/${userId}`, {
    next: { revalidate: 3600 }, // Cache przez 1 godzinę
  })
  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>Członek od {new Date(user.createdAt).toLocaleDateString('pl-PL')}</p>
    </section>
  )
}

Trzy kluczowe punkty:

  • Funkcja cache() Reacta deduplikuje identyczne wywołania podczas pojedynczego renderu serwerowego
  • next: { revalidate } kontroluje czas cache po stronie Next.js
  • Błędy w asynchronicznym Server Component aktywują najbliższy error.tsx

Pułapka serializacji: co nie przekracza granicy

Dane wymieniane między Server a Client Components muszą być serializowalne do JSON. Oto co powoduje ciche błędy lub crashe.

tsx
// PUŁAPKA: przekazywanie nieserializowalnych typów
// Funkcja — nie działa
<ClientComp onSubmit={async (data) => { /* server action */ }} />
// Zamiast tego użyć importowanej Server Action
import { submitForm } from '@/lib/actions/form'
<ClientComp onSubmit={submitForm} />

// Obiekt Date — nie działa
<ClientComp createdAt={new Date()} />
// Łańcuch ISO — działa
<ClientComp createdAt={new Date().toISOString()} />

// Map, Set, RegExp — nie działa
<ClientComp data={new Map([['key', 'value']])} />
// Zwykły obiekt lub tablica — działa
<ClientComp data={{ key: 'value' }} />

Server Actions (funkcje oznaczone dyrektywą "use server") są wyjątkiem: mogą być przekazywane jako propsy do Client Component, ponieważ Next.js przekształca je w endpointy HTTP.

Gotowy na rozmowy o React / Next.js?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Streaming i Suspense: wzorce progresywnego ładowania

Streaming SSR z Suspense wysyła HTML do przeglądarki progresywnie. Optymalny wzorzec wykorzystuje granularne granice Suspense wokół każdej sekcji asynchronicznej.

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>
  )
}

Każda sekcja ładuje się niezależnie. Jeśli RevenueChart zajmuje 3 sekundy, a UserStats 200 ms, statystyki pojawiają się natychmiast bez czekania na wykres.

Suspense a SEO

Treść wewnątrz granicy Suspense jest renderowana po stronie serwera i zawarta w początkowym HTML. Crawlery widzą pełną treść. Streaming wpływa tylko na szybkość dostarczenia do przeglądarki, nie na widoczność SEO.

Debugowanie produkcyjne: śledzenie problemów RSC

Błędy RSC są często niejasne. Trzy techniki diagnostyczne sprawdzają się na produkcji.

1. Identyfikacja niezgodności hydracji

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. Logowanie payloadu RSC

W Next.js 15 włączyć logowanie RSC w next.config.ts:

next.config.tstypescript
const nextConfig = {
  logging: {
    fetches: {
      fullUrl: true, // Pokazuje pełne URL-e fetch
    },
  },
}

export default nextConfig

3. Sprawdzanie rozmiaru payloadu

Zbyt duży payload RSC (> 128 KB) pogarsza wydajność. Monitorować żądania sieciowe z content type text/x-component w DevTools.

Zaawansowany wzorzec: kompozycja z Server Actions

Server Actions połączone z Server Components tworzą naturalny wzorzec CQRS: odczyty na serwerze (RSC), zapisy przez 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">Usuń</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')
}

Wywołanie revalidatePath wyzwala świeży render Server Component z zaktualizowanymi danymi, bez pełnego przeładowania strony.

Dla głębszego przygotowania do rozmowy kwalifikacyjnej z tych tematów warto sięgnąć po moduł Next.js Server Actions oraz moduł Next.js Data Fetching na SharpSkill. Oficjalna dokumentacja React opisuje pełną specyfikację Server Components.

Podsumowanie

  • Trzymać Client Components małe i izolowane na dole drzewa komponentów
  • Używać wzorca slot (children), aby zachować Server Components wewnątrz wrappera klienta
  • Zawsze weryfikować serializowalność propsów przekraczających granicę server-client
  • Umieszczać granularne granice Suspense wokół każdej niezależnej sekcji asynchronicznej
  • Monitorować rozmiar payloadu RSC na produkcji (cel < 128 KB)
  • Łączyć Server Components (odczyty) i Server Actions (zapisy) dla naturalnego wzorca CQRS
  • Używać funkcji cache() Reacta do deduplikacji żądań w obrębie pojedynczego renderu serwerowego

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

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

Udostępnij

Powiązane artykuły