React Server Components vs Client Components: Quando Usar Cada Um no Next.js

Conteúdo técnico toda semana
Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.
Sem spam. Cancele a qualquer momento com 1 clique.
Neste artigo
O erro que custa kilobytes e latência
No App Router do Next.js, todo componente é Server Component por padrão. Adicionar "use client" no topo de um arquivo parece inofensivo: uma linha, sem erro no terminal, sem warning. O problema aparece depois, quando o bundle do cliente cresce sem necessidade, quando dados que poderiam ser buscados no servidor passam a exigir useEffect + loading state, e quando a árvore de componentes vira uma mistura confusa de responsabilidades.
A distinção entre Server Components (RSC) e Client Components não é cosmética. Ela define onde o código executa, o que vai para o bundle do navegador, e como os dados fluem pela aplicação. Se você está trabalhando com o App Router do Next.js, essa decisão impacta cada rota que você cria.
Como o Next.js decide o que roda onde
O runtime do Next.js separa dois ambientes de execução: o servidor (Node.js ou Edge runtime) e o cliente (navegador). Server Components executam exclusivamente no servidor. O resultado é HTML serializado via um protocolo chamado RSC Payload, que o React no cliente hidrata sem precisar do código-fonte do componente.
Client Components executam nos dois ambientes: primeiro no servidor (para gerar o HTML inicial via SSR), depois no cliente (para hidratar e tornar o componente interativo). O código inteiro do Client Component, incluindo dependências, vai para o bundle JavaScript do navegador.
// app/products/page.tsx
// Server Component por padrão: sem "use client", sem useState, sem useEffect.
// O Next.js executa isso no servidor e envia HTML + RSC Payload.
import { ProductList } from "./product-list";
import { db } from "@/lib/database";
export default async function ProductsPage() {
// Acesso direto ao banco, sem API intermediária.
// Isso só funciona porque o código nunca chega ao navegador.
const products = await db.product.findMany({
where: { active: true },
orderBy: { createdAt: "desc" },
take: 50,
});
return (
<main>
<h1>Produtos</h1>
<ProductList products={products} />
</main>
);
}// app/products/product-list.tsx
"use client";
// Este componente precisa de "use client" porque usa useState para filtro local.
// Ele recebe os dados já carregados via props: o fetch aconteceu no servidor.
import { useState } from "react";
type Product = {
id: string;
name: string;
price: number;
category: string;
};
export function ProductList({ products }: { products: Product[] }) {
const [filter, setFilter] = useState("");
const filtered = products.filter((p) =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
return (
<div>
<input
type="text"
placeholder="Filtrar por nome"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<ul>
{filtered.map((product) => (
<li key={product.id}>
{product.name} — R$ {product.price.toFixed(2)}
</li>
))}
</ul>
</div>
);
}A fronteira entre os dois mundos é o "use client". Tudo abaixo dessa diretiva, incluindo componentes filhos importados, entra no bundle do cliente. Tudo acima permanece no servidor.
Matriz de decisão: Server ou Client?
A escolha não depende de gosto. Depende do que o componente faz.
| Necessidade do componente | Server Component | Client Component |
|---|---|---|
| Buscar dados de banco/API interna | Sim (acesso direto, sem fetch extra) | Não (exigiria API route + useEffect) |
Usar useState, useEffect, useReducer | Não | Sim |
| Manipular eventos do DOM (onClick, onChange) | Não | Sim |
| Acessar browser APIs (localStorage, geolocation) | Não | Sim |
| Renderizar conteúdo estático ou markdown | Sim (zero JS no cliente) | Desnecessário |
| Usar bibliotecas pesadas de UI (editores, gráficos) | Evitar (o peso vai pro bundle) | Sim (mas isole o componente) |
| Acessar variáveis de ambiente secretas | Sim (process.env sem NEXT_PUBLIC_) | Não (expõe no bundle) |
| Exibir dados sensíveis antes de filtrar | Sim (filtra no servidor) | Não (dados chegam ao cliente) |
A regra operacional: comece com Server Component. Adicione "use client" apenas quando o componente precisar de interatividade, estado local ou APIs do navegador.
Composição: Server Components que contêm Client Components
O padrão mais útil é compor Server Components como containers e Client Components como folhas interativas. O Server Component busca dados e monta a estrutura. O Client Component recebe dados via props e cuida da interação.
// app/dashboard/page.tsx
// Server Component: busca métricas e delega visualização para Client Component
import { DashboardCharts } from "./dashboard-charts";
import { getMetrics } from "@/lib/analytics";
export default async function DashboardPage() {
const metrics = await getMetrics({ period: "30d" });
return (
<section>
<h1>Dashboard</h1>
{/* O componente de gráficos precisa de interatividade (hover, zoom),
mas os dados já chegam prontos do servidor */}
<DashboardCharts data={metrics} />
</section>
);
}// app/dashboard/dashboard-charts.tsx
"use client";
// recharts é uma lib pesada (~200kb). Ao isolar aqui, apenas esta parte
// entra no bundle do cliente. O resto da página é Server Component.
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from "recharts";
type MetricPoint = {
date: string;
pageViews: number;
uniqueUsers: number;
};
export function DashboardCharts({ data }: { data: MetricPoint[] }) {
return (
<ResponsiveContainer width="100%" height={400}>
<LineChart data={data}>
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="pageViews" stroke="#2563eb" />
<Line type="monotone" dataKey="uniqueUsers" stroke="#16a34a" />
</LineChart>
</ResponsiveContainer>
);
}Esse padrão mantém a biblioteca recharts fora de qualquer Server Component e evita que o fetch de dados exija uma API route separada. Se você precisa de autenticação nessa rota, o check acontece no Server Component antes de qualquer dado ser enviado ao cliente.
O que NÃO fazer
Anti-pattern 1: marcar a página inteira como Client Component
// app/blog/[slug]/page.tsx
"use client";
// ERRADO: toda a página vira Client Component.
// Consequências:
// 1. Não pode usar async/await direto (precisa de useEffect)
// 2. Todo o código desta página vai pro bundle do cliente
// 3. Perde o benefício de streaming do App Router
import { useEffect, useState } from "react";
export default function BlogPost({ params }: { params: { slug: string } }) {
const [post, setPost] = useState<any>(null);
useEffect(() => {
fetch(`/api/posts/${params.slug}`)
.then((res) => res.json())
.then(setPost);
}, [params.slug]);
if (!post) return <p>Carregando...</p>;
return <article dangerouslySetInnerHTML={{ __html: post.content }} />;
}A versão correta mantém o fetch no servidor:
// app/blog/[slug]/page.tsx
// CORRETO: Server Component. Dados buscados no servidor, HTML enviado pronto.
import { notFound } from "next/navigation";
import { db } from "@/lib/database";
import { formatDate } from "@/lib/utils";
export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
const post = await db.post.findUnique({
where: { slug: params.slug, published: true },
});
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<time dateTime={post.createdAt.toISOString()}>
{formatDate(post.createdAt)}
</time>
<div dangerouslySetInnerHTML={{ __html: post.contentHtml }} />
</article>
);
}Zero JavaScript no cliente para uma página de blog. O conteúdo chega renderizado. O SSR aqui faz sentido exatamente por isso: conteúdo estático, SEO relevante, sem interatividade necessária.
Anti-pattern 2: elevar o "use client" além do necessário
// components/header.tsx
"use client";
// ERRADO: o header inteiro vira Client Component só porque o botão de menu
// precisa de useState. O logo, a navegação estática, tudo vai pro bundle.
import { useState } from "react";
import { Logo } from "./logo";
import { NavLinks } from "./nav-links";
import { MobileMenu } from "./mobile-menu";
export function Header() {
const [menuOpen, setMenuOpen] = useState(false);
return (
<header>
<Logo />
<NavLinks />
<button onClick={() => setMenuOpen(!menuOpen)}>Menu</button>
{menuOpen && <MobileMenu />}
</header>
);
}A correção: isolar apenas o botão interativo.
// components/header.tsx
// CORRETO: Server Component. Só o toggle é Client Component.
import { Logo } from "./logo";
import { NavLinks } from "./nav-links";
import { MobileMenuToggle } from "./mobile-menu-toggle";
export function Header() {
return (
<header>
<Logo />
<NavLinks />
<MobileMenuToggle />
</header>
);
}// components/mobile-menu-toggle.tsx
"use client";
// Apenas este componente entra no bundle do cliente.
// Logo e NavLinks permanecem como Server Components: zero JS.
import { useState } from "react";
import { MobileMenu } from "./mobile-menu";
export function MobileMenuToggle() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(!open)} aria-expanded={open}>
Menu
</button>
{open && <MobileMenu />}
</>
);
}A diferença parece sutil, mas em uma aplicação com 30 rotas, cada componente desnecessariamente marcado como "use client" acumula JavaScript no bundle.
Server Actions: quando o Client Component precisa mutar dados
Client Components não podem acessar o banco diretamente, mas podem chamar Server Actions. Uma Server Action é uma função assíncrona marcada com "use server" que executa no servidor, mesmo quando invocada a partir de um Client Component.
// app/products/actions.ts
"use server";
// Server Action: executa no servidor, pode acessar banco, validar, etc.
import { db } from "@/lib/database";
import { revalidatePath } from "next/cache";
import { z } from "zod";
const addToCartSchema = z.object({
productId: z.string().uuid(),
quantity: z.number().int().min(1).max(99),
});
export async function addToCart(formData: FormData) {
const parsed = addToCartSchema.safeParse({
productId: formData.get("productId"),
quantity: Number(formData.get("quantity")),
});
if (!parsed.success) {
return { error: "Dados inválidos" };
}
await db.cartItem.upsert({
where: { productId: parsed.data.productId },
update: { quantity: { increment: parsed.data.quantity } },
create: parsed.data,
});
revalidatePath("/cart");
return { success: true };
}// app/products/add-to-cart-button.tsx
"use client";
// Client Component que chama a Server Action via form action.
// useFormStatus dá feedback de loading sem gerenciar estado manual.
import { useFormStatus } from "react-dom";
import { addToCart } from "./actions";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Adicionando..." : "Adicionar ao carrinho"}
</button>
);
}
export function AddToCartButton({ productId }: { productId: string }) {
return (
<form action={addToCart}>
<input type="hidden" name="productId" value={productId} />
<input type="hidden" name="quantity" value="1" />
<SubmitButton />
</form>
);
}Server Actions eliminam a necessidade de criar API routes para mutações simples. Se a mutação é complexa, envolve filas ou processamento assíncrono, uma API route com mensageria continua sendo a melhor opção.
Passando Server Components como children de Client Components
Um padrão que confunde muita gente: você pode passar um Server Component como children de um Client Component. O Server Component renderiza no servidor, e o Client Component recebe o resultado serializado.
// app/settings/page.tsx
// Server Component que compõe um layout interativo com conteúdo estático
import { Tabs } from "./tabs";
import { ProfileSettings } from "./profile-settings";
import { SecuritySettings } from "./security-settings";
export default function SettingsPage() {
return (
<Tabs
tabs={[
// ProfileSettings e SecuritySettings são Server Components.
// Eles renderizam no servidor e chegam como RSC Payload.
{ label: "Perfil", content: <ProfileSettings /> },
{ label: "Segurança", content: <SecuritySettings /> },
]}
/>
);
}Isso funciona porque o React serializa o output dos Server Components antes de enviá-los ao Client Component. O Client Component (Tabs) não precisa conhecer a implementação dos filhos: recebe nós React já resolvidos.
Checklist de fronteira: onde colocar o "use client"
- Identifique quais componentes usam hooks (
useState,useEffect,useRef,useContext). - Identifique quais componentes usam event handlers (
onClick,onChange,onSubmit). - Identifique quais componentes dependem de browser APIs.
- Marque apenas esses com
"use client". - Empurre o
"use client"o mais fundo possível na árvore de componentes. Se só um botão precisa de estado, isole o botão. - Dados que vêm do banco ou de APIs internas devem ser buscados em Server Components e passados via props.
- Bibliotecas que não usam hooks nem browser APIs (formatadores de data, parsers de markdown) funcionam em Server Components sem
"use client".
Se você usa Supabase como backend, o client do Supabase com createServerClient funciona em Server Components. O createBrowserClient é para Client Components. Misturar os dois é fonte de bugs silenciosos.
Quando a escolha muda com escala
Para aplicações com poucas rotas e pouca interatividade (blogs, landing pages, documentação), quase tudo pode ser Server Component. O JavaScript do cliente fica próximo de zero.
Para aplicações com interatividade densa (dashboards com drag-and-drop, editores de texto, ferramentas de design), a maioria dos componentes da área de trabalho será Client Component. O ganho dos Server Components fica nos layouts, na autenticação e no carregamento inicial de dados. O middleware do Next.js resolve autenticação e redirecionamento antes mesmo de qualquer componente renderizar.
Para aplicações mistas (e-commerce, SaaS com áreas públicas e privadas), a fronteira server/client tende a coincidir com a fronteira entre conteúdo e interação. Páginas de produto, listagens e conteúdo editorial ficam como Server Components. Carrinho, filtros, formulários e notificações ficam como Client Components.
FAQ
Posso usar useContext em Server Components?
Não. Context é uma API do React no cliente. Se você precisa compartilhar dados entre Server Components, passe via props ou use o módulo cache do React para deduplicar fetches dentro de uma mesma requisição. Para compartilhar dados entre Server e Client Components, passe via props na fronteira.
Bibliotecas de terceiros sem "use client" funcionam em Server Components?
Depende. Se a biblioteca usa internamente useState, useEffect ou browser APIs, ela vai quebrar em Server Components mesmo sem a diretiva. O erro será claro: "useState is not a function" ou similar. Nesses casos, crie um wrapper com "use client" que importa a biblioteca.
Server Components fazem fetch a cada requisição?
Por padrão no Next.js 15, fetch em Server Components não aplica cache automático (mudança em relação ao Next.js 14). Você controla o comportamento com { cache: 'force-cache' } ou com revalidate. Para dados que mudam raramente, use export const revalidate = 3600 no arquivo da rota.
Posso importar um Server Component dentro de um Client Component?
Não diretamente. Quando você importa um componente dentro de um arquivo com "use client", esse componente é tratado como Client Component independentemente de ter ou não a diretiva. Para contornar, passe o Server Component como children ou via prop de render a partir de um Server Component pai.
Server Components substituem API routes?
Para leitura de dados, sim, na maioria dos casos. Server Components acessam banco e APIs internas diretamente, sem a indireção de uma API route. Para mutações, Server Actions cobrem casos simples. API routes continuam necessárias quando você precisa expor endpoints para clientes externos, webhooks ou integrações com sistemas de mensageria.
A posição que defendo
A fronteira "use client" é a decisão de arquitetura mais impactante no App Router do Next.js, e a mais fácil de errar por preguiça. Marcar um componente pai como Client Component porque um filho precisa de onClick é o equivalente moderno de importar o lodash inteiro para usar _.get. A disciplina de empurrar o "use client" para as folhas da árvore não é perfeccionismo: é a diferença entre enviar 50kb e 250kb de JavaScript para o navegador em cada page load.
Server Components são o padrão por um motivo. Trate "use client" como uma concessão necessária, não como o modo normal de escrever React.

Escrito por
Marcos Soares
Fullstack Developer · CEO da Agência Poti
Fullstack Developer e CEO da Agência Poti. Mais de 20 anos construindo arquiteturas cloud-native com React, Next.js e sistemas distribuídos. Parceiro comercial do estúdio iellou design. Fundador do Vivo de Código.
Comentários
Participe da discussão
Seja o primeiro a comentar!
Continue Aprofundando
Guias de integração relacionados
Conteúdo técnico toda semana
Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.
Sem spam. Cancele a qualquer momento com 1 clique.


