Server Component e Client Component no Next.js
1. Visão geral
Next.js App Router renderiza componentes no servidor por padrão, bem diferente do modelo “tudo é cliente” do Pages Router anterior. Este artigo ajuda você a entender a diferença entre Server Component e Client Component, quando é obrigatório usar "use client", e como combinar ambos os tipos de componentes na mesma árvore de renderização sem erros. Depois de ler, você saberá decidir se um componente deve ficar no servidor ou cliente, e como passar dados através dessa fronteira corretamente.
2. O que é Server Component
Server Component é o tipo de componente padrão no App Router (pasta app/). Ele executa completamente no servidor e não envia JavaScript de si mesmo para o navegador.
// app/products/page.tsx
async function getProducts() {
const res = await fetch("https://api.shop.local/products", { cache: "no-store" });
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<ul>
{products.map((p: { id: number; name: string }) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
Alguns pontos importantes:
ProductsPageé umaasync function— Server Component permiteawaitdireto, sem precisar deuseEffectouuseStatepara buscar dados.- O comando
fetché executado no servidor, dados da chave de API ou lógica sensível nunca são expostos ao navegador. - Sem a linha
"use client"no início do arquivo, Next.js entende automaticamente que é um Server Component.
2.1. Por que priorizar Server Component
Server Component ajuda a reduzir o tamanho do JavaScript enviado ao cliente, porque o próprio componente (e seus imports, se não forem clientizados) não é incluído no bundle do navegador. Ele também acessa diretamente banco de dados, sistema de arquivos, ou variáveis de ambiente sensíveis sem precisar exposição através de rota de API.
Request → Renderizar no servidor (fetch, DB query) → HTML + RSC payload → Navegador
O navegador recebe apenas HTML pré-renderizado junto com um payload descrevendo a árvore de componentes (RSC payload), não recebe o código-fonte JS do Server Component.
3. O que é Client Component
Client Component é um componente que precisa executar no navegador — para usar state, effect, event handler, ou APIs da Web como localStorage, window. Você declara isso com a diretiva "use client" no início do arquivo.
// app/components/LikeButton.tsx
"use client";
import { useState } from "react";
export default function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? "❤️ Já gostei" : "🤍 Gostei"}
</button>
);
}
Explicação:
"use client"deve ser a primeira linha do arquivo (antes de qualquer import), marcando uma fronteira — tudo que é importado deste arquivo também é incluído no bundle do cliente.useStatesó funciona em Client Component porque precisa de JavaScript rodando no navegador para gerenciar re-renderizações.onClické um event handler — um sinal seguro de que você precisa de um Client Component.
Nota:
"use client"não significa que o componente apenas roda no cliente. Next.js ainda o renderiza na primeira vez no servidor (SSR) para ter HTML rápido, depois faz “hydrate” no navegador para o componente se tornar interativo.
4. Quando é obrigatório usar Client Component
Há alguns casos em que você deve adicionar "use client", senão encontrará erros de build ou runtime:
Error: useState can not be used in Server Components.
Esses casos incluem:
- Usar React hooks:
useState,useEffect,useReducer,useContext… - Anexar event handlers:
onClick,onChange,onSubmit… - Usar APIs da Web que só existem no navegador:
window,document,localStorage. - Usar custom hooks que usam qualquer uma das acima.
- Usar bibliotecas que só rodam no cliente (ex: algumas bibliotecas de gráficos que dependem de
window).
Por outro lado, se um componente apenas recebe props e renderiza JSX puro, sem hooks, sem eventos — deixe-o como Server Component, mesmo que esteja profundo em uma árvore aninhada com outros Client Components.
5. Combinando Server e Client Component
O grande ponto deste modelo é que você pode aninhar Server Component dentro de Client Component através de children (composição), em vez de importar diretamente.
// app/components/Modal.tsx
"use client";
import { useState } from "react";
export default function Modal({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Abrir detalhes</button>
{open && <div className="modal">{children}</div>}
</>
);
}
// app/orders/[id]/page.tsx
import Modal from "@/app/components/Modal";
async function getOrder(id: string) {
const res = await fetch(`https://api.shop.local/orders/${id}`);
return res.json();
}
export default async function OrderPage({ params }: { params: { id: string } }) {
const order = await getOrder(params.id);
return (
<Modal>
{/* OrderDetail ainda é um Server Component, apenas "passado" como children */}
<p>ID do pedido: {order.id}</p>
<p>Valor total: {order.total.toLocaleString("pt-BR")} R$</p>
</Modal>
);
}
Explicação do mecanismo:
Modalé um Client Component que gerencia o estado aberto/fechado.- O conteúdo dentro (
<p>ID do pedido...) é renderizado emOrderPage— um Server Component — e depois passado paraModalcomochildren. - Next.js renderiza essa parte
childrenno servidor primeiro, depois “encaixa” o resultado na posição{children}do Client Component ao fazer hydrate. Assim,OrderService.getOrdernão precisa ser clientizado.
Nota: Você não pode importar diretamente um
asyncServer Component e usá-lo dentro de um Client Component (ex:import OrderDetail from "./OrderDetail"em um arquivo com"use client"). Next.js vai considerarOrderDetailcomo sendo puxado para o bundle do cliente e reportará um erro porqueasync function componentnão é válido no cliente. Composição viachildrenoupropsé a forma correta.
6. Passando dados de Server Component para Client Component
Props passados de um Server Component para um Client Component devem ser serializáveis — ou seja, convertíveis em JSON. Você não pode passar funções, instâncias de classe, ou objetos Date complexos.
// app/components/PriceTag.tsx
"use client";
export default function PriceTag({ amount, currency }: { amount: number; currency: string }) {
return <span>{amount.toLocaleString("pt-BR")} {currency}</span>;
}
// app/products/[id]/page.tsx
import PriceTag from "@/app/components/PriceTag";
export default async function ProductDetailPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.shop.local/products/${params.id}`).then(r => r.json());
return (
<div>
<h1>{product.name}</h1>
<PriceTag amount={product.price} currency="BRL" />
</div>
);
}
Aqui amount (número) e currency (string) são dados primitivos e serializáveis. Se você tentar passar um callback de função de um Server Component para PriceTag, Next.js reportará um erro em build/runtime:
Error: Functions cannot be passed directly to Client Components
unless you explicitly expose it by marking it with "use server".
Para chamar lógica do servidor a partir do cliente (ex: enviar formulário), use Server Action — marcado com "use server" — em vez de passar uma função comum.
7. Server Action — ponte de volta
Quando um Client Component precisa chamar o servidor novamente (escrever dados, chamar DB), use Server Action em vez de criar uma rota de API separada.
// app/actions/order-actions.ts
"use server";
export async function cancelOrder(orderId: string) {
await fetch(`https://api.shop.local/orders/${orderId}/cancel`, { method: "POST" });
}
// app/components/CancelButton.tsx
"use client";
import { cancelOrder } from "@/app/actions/order-actions";
import { useState } from "react";
export default function CancelButton({ orderId }: { orderId: string }) {
const [pending, setPending] = useState(false);
async function handleClick() {
setPending(true);
await cancelOrder(orderId);
setPending(false);
}
return (
<button onClick={handleClick} disabled={pending}>
{pending ? "Cancelando..." : "Cancelar pedido"}
</button>
);
}
cancelOrderé marcado com"use server", Next.js gera automaticamente um endpoint oculto para o Client Component chamar sem você precisar escrever um manipulador de rota.CancelButtonimporta e chama como uma função comum — o código ainda se lê naturalmente, mesmo que na realidade seja uma chamada de rede.
8. Escrevendo testes para lógica separada
Como Server Component geralmente contém lógica de fetch/transformação de dados, separar a lógica pura (sem JSX) torna os testes mais fáceis do que testar toda a renderização do componente.
// app/lib/pricing.ts
export function formatPrice(amount: number, currency: string): string {
if (amount < 0) {
throw new Error("valor não pode ser negativo");
}
return `${amount.toLocaleString("pt-BR")} ${currency}`;
}
// app/lib/pricing.test.ts
import { describe, it, expect } from "vitest";
import { formatPrice } from "./pricing";
describe("formatPrice", () => {
it("formata valor monetário válido", () => {
expect(formatPrice(150000, "BRL")).toBe("150.000,00 BRL");
});
it("lança erro quando valor é negativo", () => {
expect(() => formatPrice(-1, "BRL")).toThrow("valor não pode ser negativo");
});
});
Se o projeto usar Jest + Testing Library para Client Components (ex: LikeButton), você testa como um componente React comum — Jest não precisa saber nada sobre "use client", pois essa diretiva só tem significado para o bundler do Next.js e não afeta o runtime de testes.
// app/components/LikeButton.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import LikeButton from "./LikeButton";
describe("LikeButton", () => {
it("muda o estado quando clicado", () => {
render(<LikeButton />);
const button = screen.getByRole("button");
expect(button).toHaveTextContent("Gostei");
fireEvent.click(button);
expect(button).toHaveTextContent("Já gostei");
});
});
9. Erros comuns
Vários erros que são comuns ao mudar para o App Router:
- Esquecer
"use client"ao usar hooks: errouseState can not be used in Server Components— adicione a diretiva no início do arquivo que contém o hook, não no arquivo que o chama. - Colocar
"use client"muito alto na árvore de componentes: se você marcar até umlayout.tsxcomo cliente, tudo dentro dele (incluindo Server Components filhos) também é puxado para o bundle do cliente se importado diretamente, fazendo o bundle inchar desnecessariamente. Só deve clientizar o componente menor necessário (ex: botão, input de formulário), não a página inteira. - Passar função ou objeto não serializável como props de um Server Component para um Client Component — use Server Action (
"use server") em vez de passar um callback. - Importar um Server Component dentro de um arquivo
"use client"e esperar que ele ainda rode no servidor — não funciona, deve usar composição viachildren/props. - Chamar
fetchcom chave secreta em um Client Component — a chave de API será exposta no bundle JS enviado para o navegador. Operações que precisam de segredos sempre devem estar em um Server Component ou Server Action.
Nota: Não é seguro “converter tudo para Client Component para parecer com código antigo”. Cada Client Component desnecessário aumenta o tamanho do bundle, desacelera o tempo de interação (TTI), e perde os benefícios de renderizar no servidor.
10. Conclusão
Server Component é o padrão no App Router, adequado para buscar dados, acessar recursos sensíveis, e reduzir o JS do bundle. Client Component deve ser usado apenas quando há state, effect, ou event handler. Esses dois tipos se combinam através de composição (children, props serializáveis) e Server Action como ponte para Client Component chamar lógica do servidor. Dominar essa fronteira ajuda a otimizar o tamanho do bundle e manter dados sensíveis seguros no servidor.