React Server Components em produção: padrões e armadilhas

React Server Components em produção: padrões testados em batalha, anti-padrões comuns e estratégias de depuração para aplicações Next.js 15 robustas.

Padrões e armadilhas dos React Server Components em produção

Os React Server Components (RSC) mudam de forma fundamental como funciona a renderização no servidor no Next.js 15, mas a adoção em produção revela armadilhas que a documentação oficial nem sempre cobre. Este artigo destrincha os padrões que funcionam, os que quebram e como diagnosticar problemas antes que cheguem à produção.

Server Components e Client Components

Um Server Component roda exclusivamente no servidor e envia zero JavaScript ao navegador. Um Client Component (marcado com "use client") roda dos dois lados. A regra: manter os Client Components o menores e mais abaixo possível na árvore.

A fronteira servidor-cliente: entender o padrão de boundary

A armadilha mais comum com RSC envolve a fronteira entre Server e Client Components. Assim que um componente carrega a diretiva "use client", todos os filhos importados também viram Client Components, mesmo sem a diretiva.

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: acesso direto ao banco */}
      <ProductDetails product={product} />
      {/* Client Component: interatividade isolada */}
      <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 ? 'Adicionando...' : `Adicionar ao carrinho — $${price}`}
    </button>
  )
}

O padrão-chave: passar dados como props serializáveis do Server Component para o Client Component. Funções, classes e objetos Date não atravessam essa fronteira.

Anti-padrão: o wrapper Client Component desnecessário

Um erro frequente é criar um Client Component que envolve filhos Server Components, forçando toda a subárvore para o lado cliente.

PageWrapper.tsx — ANTI-PADRÃOtsx
'use client'

import { useState } from 'react'

// Todo o conteúdo filho vai para o lado cliente
export function PageWrapper({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState('light')
  return (
    <div className={theme}>
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        Alternar tema
      </button>
      {children}
    </div>
  )
}
children como slot

A solução: passar Server Components como children (padrão slot). Filhos passados como props continuam sendo Server Components mesmo quando o pai é um Client Component. O código acima funciona corretamente desde que children venha de um pai Server Component.

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

export default function Layout() {
  return (
    <PageWrapper>
      {/* Continua Server Component apesar do wrapper cliente */}
      <HeavyServerContent />
    </PageWrapper>
  )
}

Esse padrão de composição preserva os benefícios da renderização no servidor para conteúdo pesado, ao mesmo tempo em que habilita interatividade no nível do wrapper.

Manuseio de dados assíncronos: o padrão fetch no componente

React 19 e Next.js 15 suportam async/await diretamente em Server Components. Esse padrão simplifica a busca de dados em comparação com a antiga abordagem getServerSideProps.

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

// Deduplica chamadas idênticas dentro do mesmo render
const getUser = cache(async (userId: string) => {
  const res = await fetch(`https://api.example.com/users/${userId}`, {
    next: { revalidate: 3600 }, // Cache por 1 hora
  })
  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>Membro desde {new Date(user.createdAt).toLocaleDateString('pt-BR')}</p>
    </section>
  )
}

Três pontos críticos:

  • O cache() do React deduplica chamadas idênticas durante um único render do servidor
  • next: { revalidate } controla a duração do cache do lado do Next.js
  • Erros num Server Component assíncrono acionam o error.tsx mais próximo

Armadilha de serialização: o que não atravessa a fronteira

Dados trocados entre Server e Client Components precisam ser serializáveis em JSON. Veja o que provoca erros silenciosos ou crashes.

tsx
// ARMADILHA: passar tipos não serializáveis
// Função — não funciona
<ClientComp onSubmit={async (data) => { /* server action */ }} />
// Usar uma Server Action importada no lugar
import { submitForm } from '@/lib/actions/form'
<ClientComp onSubmit={submitForm} />

// Objeto Date — não funciona
<ClientComp createdAt={new Date()} />
// String ISO — funciona
<ClientComp createdAt={new Date().toISOString()} />

// Map, Set, RegExp — não funciona
<ClientComp data={new Map([['key', 'value']])} />
// Objeto plano ou array — funciona
<ClientComp data={{ key: 'value' }} />

As Server Actions (funções marcadas com "use server") são a exceção: podem ser passadas como props para um Client Component porque o Next.js as transforma em endpoints HTTP.

Pronto para mandar bem nas entrevistas de React / Next.js?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Streaming e Suspense: padrões de carregamento progressivo

O streaming SSR com Suspense envia HTML ao navegador de forma progressiva. O padrão ótimo usa boundaries Suspense granulares ao redor de cada seção assíncrona.

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

Cada seção carrega de forma independente. Se RevenueChart leva 3 segundos e UserStats 200 ms, as estatísticas aparecem instantaneamente sem esperar o gráfico.

Suspense e SEO

O conteúdo dentro de um boundary Suspense é renderizado no servidor e incluído no HTML inicial. Crawlers veem o conteúdo completo. O streaming afeta apenas a velocidade de entrega ao navegador, não a visibilidade SEO.

Depuração em produção: rastrear problemas RSC

Erros RSC costumam ser crípticos. Três técnicas de diagnóstico funcionam em produção.

1. Identificar incompatibilidades de hidratação

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. Registrar o payload RSC

No Next.js 15, ativar o logging RSC em next.config.ts:

next.config.tstypescript
const nextConfig = {
  logging: {
    fetches: {
      fullUrl: true, // Mostra as URLs completas dos fetch
    },
  },
}

export default nextConfig

3. Verificar o tamanho do payload

Um payload RSC superdimensionado (> 128 KB) degrada o desempenho. Monitorar requisições de rede com o content type text/x-component no DevTools.

Padrão avançado: composição com Server Actions

Server Actions combinadas com Server Components criam um padrão CQRS natural: leituras no servidor (RSC), escritas via 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">Excluir</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')
}

A chamada a revalidatePath dispara um novo render do Server Component com dados atualizados, sem recarregar a página inteira.

Para uma preparação de entrevista mais profunda sobre esses tópicos, confira o módulo Next.js Server Actions e o módulo Next.js Data Fetching no SharpSkill. A documentação oficial do React cobre toda a especificação dos Server Components.

Conclusão

  • Manter Client Components pequenos e isolados na parte de baixo da árvore de componentes
  • Usar o padrão slot (children) para preservar Server Components dentro de um wrapper cliente
  • Sempre verificar a serializabilidade das props que atravessam a fronteira servidor-cliente
  • Colocar boundaries Suspense granulares ao redor de cada seção assíncrona independente
  • Monitorar o tamanho do payload RSC em produção (alvo < 128 KB)
  • Combinar Server Components (leituras) e Server Actions (escritas) para um padrão CQRS natural
  • Usar o cache() do React para deduplicar requisições dentro de um único render do servidor

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Tags

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

Compartilhar

Artigos relacionados