Como quase publiquei um Dashboard quebrado: O "Trap da Hidratação" no meu projeto SaaS
Recentemente, passei três semanas desenvolvendo um Dashboard Financeiro usando Next.js. No meu ambiente de desenvolvimento local, tudo parecia perfeito. Mas, no momento em que fiz o deploy do MVP para staging, notei algo estranho: alguns botões não funcionavam no primeiro clique e o layout dava um "pulo" estranho um segundo após o carregamento.
Depois de horas debugando, percebi que tinha caído em uma das armadilhas mais comuns do desenvolvimento web moderno: O Mismatch de Hidratação (Hydration Mismatch).
Um Dashboard "Inteligente"
A ideia do app era mostrar layouts diferentes baseados no tamanho da tela do usuário e no fuso horário local. Para garantir que fosse "rápido", utilizei Server-Side Rendering (SSR).
Botões "Fantasma" e Saltos de Layout
Tentei ser "esperto" verificando o objeto window ou o fuso horário do usuário diretamente dentro dos meus componentes para decidir o que renderizar. O código era algo assim:
// Meu erro: Tentando detectar "Mobile" no servidor
const Sidebar = () => {
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
return (
{isMobile ? : }
);
}
O que aconteceu em seguida foi um pesadelo técnico:
- O Servidor renderizava o DesktopSidebar (porque window é undefined no Node.js).
- O Navegador recebia o HTML da versão Desktop, mas quando o JavaScript entrava em ação, ele calculava que deveria ser o MobileMenu.
- O Conflito: O React ficou confuso. Ele tentou anexar os event listeners (cliques de botão) do menu mobile em uma estrutura de HTML que ainda era do desktop.
O resultado? A interface parecia correta visualmente após o pulo, mas nada era interativo. Os botões eram "fantasmas".
Como eu resolvi
A solução exigiu uma mudança de mentalidade. Tive que aceitar que a primeira renderização deve ser idêntica tanto no servidor quanto no cliente.
Criei um hook customizado para adiar a renderização de funcionalidades específicas do cliente até que a fase de "Hidratação" estivesse concluída:
// A Solução: Garantindo que o cliente foi "Montado"
export function useHasMounted() {
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => {
setHasMounted(true);
}, []);
return false;
}
// No componente:
const Sidebar = () => {
const hasMounted = useHasMounted();
// Renderiza um estado neutro ou esqueleto no servidor
if (!hasMounted) return ;
return window.innerWidth < 768 ? : ;
}
Como você pode evitar isso
Se você está praticando SSR ou usando frameworks como Next.js, Nuxt ou SvelteKit, siga estas regras:
- Não confie na primeira renderização: Se sua lógica depende de localStorage, window, document ou fusos horários específicos do navegador, não use isso no JSX inicial.
- Use Skeletons (Esqueletos): Renderize um estado de carregamento neutro no servidor e troque pelo conteúdo real apenas dentro de um useEffect.
- Atenção ao Console: Se você vir o aviso "Hydration failed", não ignore. Isso significa que o JavaScript da sua página provavelmente está "descolado" do seu HTML.
4.Construir esse dashboard me ensinou que, no mundo do SSR, a "velocidade" não pode vir à custa da consistência. Espero que este relato poupe alguns de vocês de uma noite longa de debug!
Alguém aqui já enfrentou um bug que só acontecia por causa desse "gap" entre o Servidor e o Cliente? Vamos conversar nos comentários.