Depois de rodar RSC em produção com Next.js App Router, a dúvida que mais volta em conversa com outros times é sobre a fronteira entre Server e Client Component. Abaixo está o mapa de decisão que eu uso, e os erros que vi a equipe cometer na transição.
Server Component renderiza no servidor, consulta banco, filesystem ou API interna, produz HTML e RSC payload, e não envia uma linha de JavaScript para o cliente. Client Component entra no bundle do cliente, roda no navegador (e também no servidor, na hidratação), e acessa useState, useEffect, handlers de evento e APIs do browser.
A consequência prática: lógica, parser pesado e chamada de banco dentro de Server Component custam zero bytes no cliente. O manual de Server Components do React e a página de use client definem essa fronteira.
Server Component quando
- O componente consulta dados direto: banco, API interna, filesystem.
- Ele usa dependência pesada sem interatividade (
marked,sharp, parser de PDF). - Ele lê variável de ambiente que não pode ir para o cliente.
- Ele compõe a página e não tem estado próprio.
// Server Component: consulta direta, sem JavaScript no cliente
async function ProductPage({ id }: { id: string }) {
const product = await db.product.findUnique({ where: { id } })
return (
<div>
<h1>{product.name}</h1>
<ProductActions product={product} />
</div>
)
}
Client Component quando
- Estado local:
useState,useReducer. - Efeito:
useEffect,useLayoutEffect. - Handler de evento:
onClick,onChange. - API do browser:
localStorage,window,navigator.
'use client'
import { useState } from 'react'
export function ProductActions({ product }: { product: Product }) {
const [quantity, setQuantity] = useState(1)
return (
<div>
<input
type="number"
value={quantity}
onChange={e => setQuantity(Number(e.target.value))}
/>
<button onClick={() => addToCart(product.id, quantity)}>
Adicionar ao carrinho
</button>
</div>
)
}
Os erros que a equipe comete
1. 'use client' em todo componente
Diretiva de precaução transforma a árvore inteira em Client Component e devolve o bundle que os RSC existem para remover. Server Component importado por Client Component vira Client Component junto. Coloque a diretiva na folha que precisa de interatividade e mantenha o resto no servidor.
2. Prop que não atravessa a fronteira
Server Component passa prop para Client Component só se a prop serializar: string, número, array, objeto simples. Função, instância de classe e objeto com método não passam, e o erro aparece em runtime com a mensagem de serialização do React.
// erro: função não atravessa a fronteira
<ClientComponent handler={someServerFunction} />
// correto: define o handler no Client Component
// ou chama Server Function a partir dele
3. Server Function confundida com busca
Server Functions rodam no servidor e aceitam chamada de Client Component. Elas servem para mutação. Leitura fica com Server Component, escrita com Server Function.
4. Cascata de await
Async direto no corpo do Server Component cria cascata quando uma chamada depende da outra por engano:
// cascata: a segunda chamada espera a primeira
const user = await fetchUser(id)
const orders = await fetchOrders(user.id)
// paralelo: as duas saem juntas
const [user, orders] = await Promise.all([
fetchUser(id),
fetchOrders(id)
])
Quando os dados não dependem um do outro, Promise.all elimina o tempo morto entre as duas chamadas. Quando a dependência é real, olhe se a segunda chamada recebe o id direto, sem passar pelo objeto da primeira.
O padrão de composição
Página com RSC bem montada tem Server Component como raiz, fazendo a consulta e compondo a estrutura. Client Components ficam nas folhas, perto da interatividade. Subseções que precisam de dados e não de interatividade voltam a ser Server Components aninhados. A página de padrões de composição do Next.js mostra esse desenho com exemplos de layout, Suspense e streaming.
Curtiu o conteúdo?
Construo produtos web e soluções com IA do jeito certo — arquitetura sólida, código sustentável e entrega real.
Vamos conversar