Misturas entre componentes server e client no Next.js App Router: consertos
Aprenda a identificar misturas entre componentes do servidor e do cliente no Next.js App Router que causam crashes em runtime, e como reestruturar componentes, actions e busca de dados.

O que é uma mistura entre apenas servidor e apenas cliente?
Uma mistura entre apenas servidor e apenas cliente acontece quando código roda no lugar errado.
No Next.js App Router, alguns componentes devem rodar no servidor (seguros para segredos, chamadas diretas ao banco de dados, APIs privadas). Outros devem rodar no navegador (cliques de botão, estado local, acesso a window). Quando essas responsabilidades se embaralham, você verá crashes, problemas de hidratação ou builds que só quebram depois do deploy.
Um modelo mental útil:
- Componentes do servidor buscam e preparam dados, então passam props simples para baixo.
- Componentes do cliente lidam com interação, mas não devem puxar código exclusivo do servidor.
Frequentemente parece tudo bem em desenvolvimento porque o modo dev pode ser mais permissivo. Hot reload, empacotamento diferente e timings podem esconder problemas de boundary. Builds de produção são mais rígidos sobre o que pode ser enviado para o navegador, e a hidratação tolera menos incompatibilidades.
Sintomas comuns:
- Página em branco após navegação (erro somente no console)
- Erros de hidratação onde a UI pisca e então quebra
- Erros 500 inesperados durante a renderização
- “window is not defined” ou “document is not defined”
- Erros de build sobre importar módulos apenas do servidor em arquivos do cliente
Código gerado por IA torna isso mais comum porque copia trechos sem respeitar limites. Um exemplo típico: adicionar "use client" a uma página inteira para consertar um erro de hook, mesmo que essa página importe um helper de banco de dados ou leia segredos.
Como o App Router separa Componentes do Servidor e do Cliente
No App Router, todo componente é um Server Component por padrão. Essa única regra explica a maioria das surpresas.
Componentes do servidor (padrão)
Componentes do servidor rodam no servidor. Use-os para buscar dados, ler cookies/headers, usar segredos de ambiente e qualquer trabalho pesado que você não quer no navegador.
Se tocar no seu banco de dados, chaves privadas de API ou sessão de autenticação, mantenha no servidor e passe os resultados como props.
Componentes do cliente (opt-in)
Um componente vira Client Component somente quando você adiciona "use client" no topo do arquivo. Componentes do cliente rodam no navegador, então podem usar estado, efeitos, event handlers e APIs do navegador como localStorage.
A boundary funciona assim:
- Um Server Component pode importar um Client Component. Tudo dentro daquele Client Component roda no client.
- Um Client Component não pode importar um Server Component ou módulos apenas do servidor.
Geralmente você precisa de "use client" quando um componente usa hooks como useState/useEffect, eventos como onClick, ou APIs do navegador como window e document.
Você não precisa de "use client" para UI simples que só renderiza props. Uma correção comum (especialmente em protótipos gerados por IA) é manter a página e o carregamento de dados no servidor, e renderizar um pequeno Client Component só para a parte interativa (um filtro, modal ou editor inline).
Padrões de crash em runtime que você pode reconhecer rapidamente
A maioria dos crashes do App Router se resume a um problema: código só-do-navegador roda no servidor, ou código só-do-servidor é empacotado para o navegador.
Padrão 1: Hooks em um Server Component
Se você vê erros como “React Hook ... is not supported in Server Components” ou “You're importing a component that needs useState/useEffect”, verifique o cabeçalho do arquivo. Se o arquivo não começa com "use client", o React o trata como Server Component.
Padrão 2: Módulos só-do-servidor puxados para código do cliente
Erros mencionando fs, path, crypto, ou “Module not found: Can't resolve 'fs'” frequentemente significam que um componente cliente importou um helper compartilhado que (talvez indiretamente) importa código exclusivo do Node.
Isso acontece quando um arquivo utils ou lib compartilhado mistura helpers de servidor e de cliente, e o cliente importa ele “só por uma função”.
Padrão 3: APIs do navegador usadas durante render no servidor
“window is not defined”, “document is not defined” e “localStorage is not defined” indicam que o código está rodando no servidor. Isso pode ser um Server Component, uma server action, ou até um módulo importado durante a renderização no servidor.
Padrão 4: Chamar lógica do servidor a partir do cliente sem uma ponte segura
Isso aparece como “You're importing a Server Action into a Client Component”, “Server-only module cannot be imported from a Client Component” ou uma chamada client-side que acidentalmente alcança uma função que nunca deveria rodar no navegador.
Passo a passo: encontre a boundary errada na árvore de componentes
As vitórias mais rápidas vêm de tornar o crash reproduzível. Teste no dev e também em um build de produção. “Funciona no dev” não é desculpa para problemas de boundary.
Quando você lê o erro, pare no primeiro arquivo que você realmente possui. Stack frames do framework são barulhentos. O primeiro arquivo no seu repo é geralmente onde a importação ou o tipo de componente errado entra na árvore.
Um fluxo simples:
- Reproduza o crash da mesma forma toda vez (mesma rota, mesma ação, mesmo estado do usuário).
- No stack trace, vá até o primeiro arquivo da sua app e note qual componente o renderizou.
- Verifique o cabeçalho do arquivo: é um Server Component padrão ou começa com "use client"?
- Siga as importações até encontrar a primeira incompatibilidade:
- importação só-do-servidor usada por código do cliente (
fs, clientes de banco,next/headers) - uso só-do-navegador dentro de código do servidor (
window,document,localStorage)
- importação só-do-servidor usada por código do cliente (
- Decida propriedade: segredos e dados no servidor, estado da UI e eventos no cliente.
Uma falha muito comum: uma página do servidor passa um cliente de banco, dados derivados de cookies ou um helper só-do-servidor para um componente cliente. Isso quebra. Busque no servidor e passe JSON simples para baixo.
Reestruturando componentes que misturam preocupações do servidor e do cliente
Crashes geralmente acontecem quando um componente tenta fazer tudo.
Uma separação confiável:
- Server Component: busca dados, verifica auth, usa segredos.
- Client Component: lida com estado, eventos, efeitos e qualquer UI dependente do DOM.
Mova o trabalho de dados para cima na árvore. Busque no Server Component (ou em uma função apenas do servidor chamada por ele) e passe o resultado como props simples. Isso mantém código só-do-servidor fora do bundle do navegador.
Depois isole a interatividade. Mantenha pedaços do cliente pequenos para não enviar a página inteira ao navegador só para fazer um botão funcionar.
Na boundary servidor→cliente, mantenha props simples: strings, números, booleanos, arrays, objetos planos. Não passe clientes de banco, objetos de request, instâncias de classe ou funções.
Exemplo: uma página de dashboard busca info do usuário, assinatura e atividade recente, mas também tem filtros, um modal e um gráfico. Busque tudo em DashboardPage (servidor) e passe { userName, plan, activityItems } para baixo. Deixe DashboardControls como Client Component para o estado dos filtros e abrir/fechar modal.
Server Actions: padrões seguros para formulários e mutações
Server Actions funcionam bem quando um usuário submete um formulário e você precisa alterar dados: criar um registro, atualizar um perfil, resetar senha, ou rodar um workflow pequeno.
Uma estrutura segura é manter a UI do formulário em um Client Component, e a mutação em um arquivo somente do servidor como uma action exportada. O cliente controla inputs, estado de carregamento e exibição de erros. O servidor faz checagens de autenticação, validação e chamadas ao banco.
// actions.ts
'use server'
export async function updateProfile(formData: FormData) {
const name = String(formData.get('name') ?? '')
// validate, check auth, write to DB
return { ok: true }
}
No lado do cliente, passe apenas o que o servidor precisa. Não passe segredos, tokens ou objetos de usuário brutos via props apenas para fazer uma action funcionar. Se a action precisa saber quem é o usuário, leia no servidor (cookies/session) dentro da própria action.
Dois hábitos evitam a maioria dos vazamentos:
- Valide entradas e verifique autorização dentro da action.
- Retorne erros seguros e amigáveis ao usuário, não stack traces.
Se quiser UI otimista, mantenha-a local e pequena. Evite transformar uma página inteira em Client Component só para mostrar um spinner.
Busca de dados no App Router sem trabalho duplicado
Muitos problemas de boundary começam com buscar dados em dois lugares.
No App Router, o padrão deve ser buscar no servidor perto da rota. Você terá paint inicial mais rápido, bundles menores no navegador e manterá segredos fora do cliente.
Busque no cliente somente quando realmente precisar (polling, widgets em tempo real, ou um botão de refresh que atualiza só uma seção).
Um bug comum: renderização no servidor busca dados, então um Client Component monta e busca os mesmos dados de novo com useEffect. Isso pode causar flicker, problemas de rate-limit e incompatibilidades confusas.
Um fluxo limpo fica assim:
- A requisição chega em uma rota
- Busca no servidor obtém os dados (DB, API interna ou terceiro)
- Server Components renderizam a página com esses dados
- Client Components lidam com interações e disparam atualizações direcionadas
Cache também pode esconder problemas durante testes. Se os dados parecerem aleatoriamente desatualizados, verifique se sua busca está em cache e se a revalidação está configurada como você espera.
Auth e segredos: o que deve ficar no servidor
Problemas de auth frequentemente começam como erro de boundary: um Client Component toca algo que nunca deveria sair do servidor. Às vezes você obtém crashes ou redirects estranhos. Às vezes você vaza segredos para o bundle do navegador.
Os vazamentos mais comuns em código gerado:
- Ler variáveis de ambiente em um Client Component
- Colocar configuração em um arquivo compartilhado importado por servidor e cliente
Se doer ver isso no DevTools, não deve ser alcançável pelo cliente.
Mantenha checagens de autenticação e lógica de papéis no servidor. O cliente pode renderizar estados de UI, mas não deve ser a fonte de verdade para “este usuário tem permissão?”.
Evite armazenar tokens sensíveis em localStorage por padrão. É fácil inspecionar e pode ser roubado por XSS.
Quebras rápidas para ficar de olho:
- Loops de redirecionamento quando servidor e cliente tentam proteger a mesma rota
- Mismatches de sessão onde o servidor renderiza um estado e o cliente hidrata outro
- Confusão entre runtime Edge vs Node para bibliotecas de auth
- “Funciona localmente, quebra em prod” quando env vars diferem e bundles do cliente mudam
Erros comuns que fazem os crashes voltarem
A maioria dos crashes recorrentes não são bugs misteriosos do framework. São os mesmos erros de boundary, consertados às pressas e reintroduzidos depois.
Alguns padrões que aparecem sempre:
- Adicionar "use client" a uma página grande para silenciar um erro de hook
- Manter helpers “compartilhados” que misturam código só-do-servidor e seguro para o cliente
- Criar ou importar um cliente de banco dentro de arquivos de componente (isso se espalha rápido pelas imports)
- Chamar
fetch()para sua própria API a partir de um Server Component por hábito, mesmo quando você pode chamar código do servidor diretamente - Consertar por tentativa e erro em vez de seguir a primeira importação ruim no stack trace
Um exemplo típico: uma página de dashboard quebra só em produção porque importa getUser() (lê cookies, só-do-servidor), mas a página foi marcada "use client" para suportar um gráfico. A correção durável é mover o gráfico para seu próprio Client Component e manter a página com prioridade server-first.
Checklist rápido antes de enviar para produção
A maioria dos crashes do App Router acontece porque um arquivo está fazendo dois trabalhos.
Verificação de boundary
Pergunte para cada componente: este arquivo pode rodar no navegador?
Se sim, ele não deve tocar segredos, variáveis de ambiente só-do-servidor, clientes de banco ou bibliotecas apenas do Node. Se você vir essas imports, mova esse trabalho para um Server Component, uma Server Action ou uma rota do servidor.
Varredura final:
- APIs do navegador (
window,document,localStorage,navigator) e hooks significam Client Component. Mantenha lógica de servidor fora. - Segredos e imports só-do-servidor significam Server Component. Passe apenas os dados que a UI precisa.
- Props que cruzam a boundary devem ser serializáveis (objetos planos, arrays, strings, números). Evite instâncias de classe,
BigInte funções. - Para escritas (formulários, updates, deletes), use uma Server Action ou uma rota do servidor.
- Teste um build de produção localmente, não apenas
next dev.
Um hábito prático
Antes de enviar, navegue pelas principais flows após um build limpo. Se uma página quebra só em modo produção, geralmente é um problema de boundary, uma prop não-serializável ou uma importação só-do-servidor vazando para o cliente.
Exemplo: consertando uma página de dashboard que está quebrando
Uma situação clássica: uma página de dashboard precisa de dados buscados no servidor e também de filtros interativos (intervalo de datas, toggles de status, busca).
O que deu errado
A primeira versão frequentemente mistura preocupações em um só arquivo. Por exemplo, app/dashboard/page.tsx busca dados do usuário no servidor, mas também usa useState, lê localStorage ou chama window.matchMedia para lembrar configurações de filtro. Isso funciona no navegador, mas a página é um Server Component por padrão, então pode quebrar com “window is not defined” ou “Hooks can only be used in a Client Component.”
Outro deslize comum: a UI de filtros é marcada com 'use client', mas ela importa um helper só-do-servidor que lê cookies ou acessa banco privado. Isso pode disparar erros do tipo “You’re importing a Server Component into a Client Component”.
Uma reestrutura simples que impede os crashes
Faça a página ficar responsável pelos dados, e o componente do cliente ficar responsável pela interatividade.
No servidor (page): busque dados e renderize.
// app/dashboard/page.tsx (Server Component)
import Filters from './Filters';
import { getDashboardData } from './data';
export default async function Page() {
const data = await getDashboardData();
return (
<>
<Filters initial={data.filters} />
{/* render table using data.items */}
</>
);
}
No cliente (filters): mantenha estado e eventos UI locais, e envie mudanças por uma Server Action.
// app/dashboard/actions.ts
'use server';
export async function updateFilters(next) {
// validate input, save, return safe data
return { ok: true };
}
Resultado: menos crashes em runtime, propriedade mais clara (o servidor busca e segredos ficam no servidor, o cliente lida com cliques) e as atualizações passam por um caminho seguro.
Próximos passos se seu código App Router continuar quebrando
Se você está brigando com o mesmo crash repetidas vezes, normalmente é um problema de boundary no projeto: código do cliente puxa módulos só-do-servidor, código do servidor importa hooks, ou mutações ficam espalhadas pelo cliente.
Codebases geradas por IA de ferramentas como Lovable, Bolt, v0, Cursor ou Replit tendem a repetir esses erros porque misturam padrões que funcionavam em setups antigos, mas não se sustentam sob a separação mais rígida do App Router.
Quando os mesmos sintomas aparecem em várias páginas, um refactor focado costuma ser mais rápido do que remendos.
Se você herdou um protótipo gerado por IA e quer um diagnóstico rápido e estruturado, FixMyMess (fixmymess.ai) se especializa em reparar e endurecer esse tipo de codebase Next.js, começando com uma auditoria grátis de código para identificar os primeiros erros de boundary e imports arriscados.