Notas sobre hidratação parcial no App Router

O erro apareceu só em produção, só no primeiro carregamento, e só para quem chegava por um link compartilhado. Levei dois dias para entender que o problema não era hidratação — era eu ter assumido que o servidor e o cliente concordavam sobre que horas eram.
O sintoma
Um Server Component renderizava uma data formatada. No cliente, o mesmo nó
vinha diferente por um dia inteiro. O React reclamava, descartava a árvore e
re-renderizava tudo do zero — o que, numa página cheia de animação, é bem mais
caro do que parece.
Hidratação não é o React "ligando" o HTML. É o React conferindo se o que ele renderizaria bate com o que já está lá. Quando não bate, ele não corrige: ele desiste e recomeça.

As três suposições
- Que
new Date()no servidor e no cliente produziriam o mesmo dia. Não produzem, se o servidor está em UTC e você não está. - Que um mismatch de hidratação era um aviso, não um erro. É um erro de performance, e um dos caros.
- Que o problema estava no componente que reclamou. Estava no componente que passou a prop.
O que eu deveria ter olhado primeiro
- A diferença exata entre o HTML do servidor e o primeiro render do cliente
- Se o valor problemático era derivado de algo não-determinístico
- Se o componente precisava mesmo ser um Client Component
A correção
Formatar no servidor e mandar a string pronta. O timezone vira uma decisão explícita em vez de um acidente do ambiente:
type Props = { publishedAt: string };
export function PublishedAt({ publishedAt }: Props) {
// Já formatado no servidor. O cliente não recalcula nada, então não há
// nada sobre o que discordar.
return <time dateTime={publishedAt}>{formatPostDate(publishedAt)}</time>;
}
A regra que ficou: se um valor depende de Date, Math.random ou
window, ele não atravessa a fronteira servidor/cliente como cálculo — ele
atravessa como resultado.
Tem mais coisa na mesma linha em todos os posts, e a documentação do React sobre hydration mismatches é mais útil do que a mensagem de erro sugere.