Destino Dev

Blog

Corrigindo mismatches de hydration no React App Router (SSR): passo a passo

Por John Pires · 13 de março de 2026

Corrigindo mismatches de hydration no React App Router (SSR): passo a passo

Por que aprender isso em 2026

O que é um mismatch de hydration

Hydration é o processo que o React usa para amarrar a árvore HTML gerada no servidor (SSR) ao JavaScript executado no cliente. Um mismatch de hydration acontece quando o HTML estático entregue pelo servidor difere da árvore que o React constrói durante a hidratação no cliente. Resultado: warnings no console, UI piscando, perda de estados ou comportamento imprevisível.

Por que o problema continua relevante em 2026

Em 2026, aplicações continuam apostando em SSR para SEO e performance percebida. O App Router (Next.js e padrões similares) popularizou Server Components e misturas de renderização. Isso aumenta a chance de divergência entre servidor e cliente — especialmente quando há comportamento dependente de ambiente, relógio, detecção de cliente, ou efeitos que alteram marcação na primeira renderização.

Entender e saber corrigir mismatches é uma habilidade operacional: evita regressões, reduz tempo gasto caçando warnings e melhora a experiência do usuário.

Configuracao do ambiente

Requisitos mínimos

Vamos usar uma base simples: Node.js 18+, npm ou pnpm, e Next.js (App Router) como exemplo prático. Você pode aplicar os conceitos a outros App Routers que suportem Server Components e hidratação. Instale com:

npx create-next-app@latest my-app --experimental-app --use-npm

Esse comando cria um projeto com App Router ativado. O importante é entender a separação entre componentes do servidor (padrão) e componentes do cliente (com "use client").

Organizando projeto para reproduzir e depurar mismatch

Crie páginas simples em /app com um Server Component que inclui um Client Component que lê hora local ou navigator. Esses pontos são fontes comuns de divergência porque o servidor não conhece informações do cliente.

Recomendo rodar o dev server e abrir o console do navegador para observar warnings sobre hydration:

npm run dev

Com o servidor em execução, abra http://localhost:3000 e repare nos logs. Se houver mismatch, o console do navegador mostrará mensagens do React detalhando o nó divergente.

Hello World progressivo

Passo 1: Server Component básico

Primeiro, um Server Component puro que renderiza conteúdo determinístico. Isso raramente causa mismatches.

Arquivo: /app/page.tsx

export default function Page() {
  return (
    <main>
      <h1>Hello from Server Component</h1>
      <p>Renderizado no servidor com dados estáveis.</p>
    </main>
  );
}

O que faz: renderiza HTML determinístico no servidor. Depois: o cliente hidrata a mesma árvore sem alterações, sem warnings.

Passo 2: Introduzindo comportamento cliente (fonte de mismatch)

Agora vamos adicionar um Client Component que mostra a hora local. Este é um exemplo clássico que cria mismatch: o servidor não sabe a hora do cliente, então HTML inicial contém uma hora diferente do que o cliente renderiza ao hidratar.

// /components/LocalClock.tsx
'use client';
import { useEffect, useState } from 'react';

export default function LocalClock() {
  const [time, setTime] = useState(() => new Date().toISOString());

  useEffect(() => {
    const id = setInterval(() => setTime(new Date().toISOString()), 1000);
    return () => clearInterval(id);
  }, []);

  return <span>{time}</span>;
}

O que faz: exibe um timestamp ISO que muda a cada segundo. Resultado: mismatch no primeiro paint porque o servidor calculou um valor diferente do cliente. Depois: o cliente substitui e você verá um flash ou um warning.

Como evitar mismatch neste caso

Estratégias:

  • Renderizar um placeholder estático no servidor e atualizar no cliente (deferir conteúdo dependente de cliente).
  • Usar suspense com lazy loading do client component para evitar diferença visível.
  • Evitar que o estado inicial dependa de new Date() no render do servidor; inicialize com null e apenas no useEffect defina.
Tip: prefira placeholders determinísticos no HTML inicial e populate com dados do cliente no useEffect. Isso elimina divergência de marcação no painting inicial.

Mini-projeto pratico

Objetivo do mini-projeto

Construiremos uma pequena página de perfil que mostra um botão de login condicionalmente. O servidor não sabe se o usuário está autenticado no cliente, então é um caso real de potencial mismatch. Vamos corrigir de forma robusta e explícita.

Arquitetura do exemplo

Estrutura simplificada:

  1. /app/page.tsx — Server Component que inclui Client Component AuthButton
  2. /components/AuthButton.tsx — Client Component que verifica localStorage/session e renderiza login/logout
  3. Correção: usar placeholder no HTML e verificar estado no useEffect

Exemplo de AuthButton que gera mismatch (anti-padrão)

Contexto: este componente lê localStorage direto durante render — causa mismatch porque o servidor não executa localStorage.

'use client';
import React from 'react';

export default function AuthButtonBad() {
  const isLogged = typeof window !== 'undefined' && !!window.localStorage.getItem('token');
  return (
    <button>{isLogged ? 'Logout' : 'Login'}</button>
  );
}

O que faz: tenta decidir o texto do botão no render. Por quê é problemático: no servidor, typeof window é undefined, code tenta compensar, mas a marcação ainda pode divergir no cliente quando localStorage existe. Isso geralmente dispara mismatch e warning.

Correção: usar state + useEffect (padrão recomendado)

Contexto: vamos renderizar um placeholder no servidor e só depois, no cliente, ajustar para o estado real. Isso evita divergência inicial da árvore HTML.

'use client';
import React, { useEffect, useState } from 'react';

export default function AuthButton() {
  const [isHydrated, setHydrated] = useState(false);
  const [isLogged, setLogged] = useState(false);

  useEffect(() => {
    setHydrated(true);
    const token = window.localStorage.getItem('token');
    setLogged(!!token);
  }, []);

  if (!isHydrated) {
    // placeholder determinístico renderizado também no server
    return <button aria-busy="true">Loading</button>;
  }

  return <button>{isLogged ? 'Logout' : 'Login'}</button>;
}

O que faz: evita que a renderização inicial dependa de dados do cliente. O servidor e o cliente veem o mesmo HTML inicial. Após hidratação, o cliente atualiza para o estado real sem mismatch.

Integração na página

Arquivo /app/page.tsx inclui o componente acima:

import AuthButton from '../components/AuthButton';

export default function Page() {
  return (
    <main>
      <h1>Perfil</h1>
      <p>Bem-vindo. Abaixo está o botão de autenticação que depende do cliente.</p>
      <AuthButton />
    </main>
  );
}

Depois: o HTML inicial contém o botão Loading; a hidratação no cliente substitui por Login/Logout sem warnings.

Erros comuns, armadilhas e dicas práticas

Leitura direta de APIs do navegador no render

Evite acessar window, document, localStorage, navigator ou APIs de data/tempo durante a renderização. Se precisar, faça a leitura dentro de useEffect ou useLayoutEffect no client component.

Sintomas típicos e como diagnosticar

Sintomas:

  • Console com mensagens de hydration mismatch do React.
  • UI piscando no primeiro render.
  • Componentes que mostram valores errados até o primeiro re-render.

Como diagnosticar:

  1. Reproduza em dev com console aberto.
  2. Remova partes do componente até encontrar o trecho que introduz a diferença (bisection rápida).
  3. Procure usos de new Date(), Math.random(), leitura de localStorage, navigator, user-agent, ou códigos condicionais com typeof window.

Dica: usar aria-busy e placeholders previsíveis

Prática: renderize placeholders previsíveis (por exemplo, "Carregando") e depois preencha com useEffect para evitar divergência de marcação.

Isso melhora percepção de velocidade e elimina warnings. Você também pode usar skeletons visuais fornecidos pela equipe de UI para deixar a experiência consistente.

Recursos para continuar estudando

Documentação e leituras oficiais

Material essencial: documentação oficial do React sobre hidratação e Server Components; documentação do Next.js App Router para entender server/client components e streaming. Esses documentos mostram APIs e recomendações de design para evitar mismatches em produção.

Ferramentas e utilitários úteis

Ferramentas que ajudam:

  • React DevTools — para inspecionar árvore e hooks.
  • Linters e regras customizadas ESLint — detectam uso direto de window/localStorage em componentes que deveriam ser server.
  • Testes end-to-end (Playwright) — para confirmar ausência de flashes e comportamento correto no fluxo de hidratação.

Recomendo escrever testes que rodem em ambiente real de browser para pegar problemas que não aparecem apenas no Node.

Conclusao pratica

Resumo das ações para evitar mismatches

Regra prática: tudo que depende do cliente deve ser inicializado no cliente via useEffect (ou carregado dinamicamente). O HTML inicial gerado no servidor precisa ser determinístico. Use placeholders, suspense e flags de hidração para controlar a transição. Evite avaliar valores voláteis durante render do servidor.

Checklist rápido para revisar seu código

  1. Não usar new Date() / Math.random() durante render do servidor para marcar DOM.
  2. Evitar acessar window/document/localStorage no render; mover para useEffect.
  3. Renderizar placeholders determinísticos no servidor.
  4. Usar 'use client' estritamente onde necessário.
  5. Testar com DevTools e em dispositivos reais para capturar edge-cases.

Seguindo essas regras você reduz drasticamente mismatches e warnings, melhora a estabilidade do app e diminui bugs difíceis de reproduzir.

Última dica prática

Quando encontrar um mismatch: isole o componente, crie uma versão que renderize apenas placeholders e introduza o estado real via useEffect. Se isso resolver, aplique o padrão no componente original. É rápido, previsível e escalável.

Com isso você tem um procedimento claro para diagnosticar e corrigir mismatches no App Router com SSR: reproduzir, isolar, substituir por placeholder determinístico, preencher no cliente e testar.

Boa prática adicional: documente a razão de usar placeholders ou client components no repositório — facilita revisões de PR e evita regressões por desconhecimento.

Assuntos: React, ssr, app-router, hydration, Performance

Leia também