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 é uma async function — Server Component permite await direto, sem precisar de useEffect ou useState para 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.
  • useState só 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 em OrderPage — um Server Component — e depois passado para Modal como children.
  • Next.js renderiza essa parte children no servidor primeiro, depois “encaixa” o resultado na posição {children} do Client Component ao fazer hydrate. Assim, OrderService.getOrder não precisa ser clientizado.

Nota: Você não pode importar diretamente um async Server Component e usá-lo dentro de um Client Component (ex: import OrderDetail from "./OrderDetail" em um arquivo com "use client"). Next.js vai considerar OrderDetail como sendo puxado para o bundle do cliente e reportará um erro porque async function component não é válido no cliente. Composição via children ou props é 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.
  • CancelButton importa 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: erro useState 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é um layout.tsx como 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 via children/props.
  • Chamar fetch com 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.

nextjsreactserver-componentclient-componentapp-router