Capítulo 70 de 456

Preventing Flash

Core Idea

Client-only state (locale, timezone, theme, persisted UI) isn't available during server rendering. Use a synchronous inline <script> (via dangerouslySetInnerHTML) that runs during HTML parsing, before first paint, to correct the DOM, paired with suppressHydrationWarning so React accepts the corrected DOM instead of throwing a hydration error.

Key Concepts

  • Inline script technique: a <script> inserted with dangerouslySetInnerHTML runs synchronously as the browser parses HTML, before first paint — the only technique that beats both hydration and the initial paint.
  • suppressHydrationWarning: tells React to keep whatever is in the DOM for that element rather than the client's computed output, avoiding a hydration error/flash; without it, React client-renders from the nearest boundary and loses other inline-script corrections in that boundary.
  • InlineScript helper: sets type="text/javascript" on server, type="text/plain" on client, to avoid React dev warnings about rendering <script> tags while still running it once on hard navigation.
  • Hard vs. soft navigation split: the inline script only executes on full page loads (parsing); on client-side <Link> navigations the Client Component's own render (toLocaleDateString()) handles it directly, and the text/plain script is a no-op.
  • Lazy useState initializer: must read from the exact same source (e.g. localStorage) as the inline script so React's initial client state matches the DOM the script already set.
  • Cookie vs. localStorage for theme: a cookie is readable server-side via cookies(), but doing so in the root layout opts the whole app out of static prerendering; reading the cookie inside the inline script instead keeps the page static with no flash.

Code Examples

export default async function Page() {
  const event = await getEvent('nextjs-conf')
  return (
    <section>
      <h1>{event.name}</h1>
      <p id="event-date" suppressHydrationWarning>
        {new Date(event.date).toLocaleDateString()}
      </p>
      <script
        dangerouslySetInnerHTML={{
          __html: `document.getElementById("event-date").textContent=new Date("${event.date}").toLocaleDateString()`,
        }}
      />
    </section>
  )
}
  • O que demonstra: script inline corrige o texto formatado por locale antes da primeira pintura, evitando o erro de hidratação de toLocaleDateString().
export function InlineScript({ html }: { html: string }) {
  return (
    <script
      type={typeof window === 'undefined' ? 'text/javascript' : 'text/plain'}
      suppressHydrationWarning
      dangerouslySetInnerHTML={{ __html: html }}
    />
  )
}
  • O que demonstra: helper reutilizável que só executa no servidor (hard nav); em navegação client-side vira text/plain e é ignorado, deixando o próprio Client Component formatar direto.
<html lang="en" data-theme="light" suppressHydrationWarning>
  <head>
    <script
      dangerouslySetInnerHTML={{
        __html: `(function(){try{var t=localStorage.getItem("theme");if(t)document.documentElement.setAttribute("data-theme",t)}catch(e){}})()`,
      }}
    />
  </head>
  <body>{children}</body>
</html>
  • O que demonstra: aplica o tema salvo em localStorage no <html> antes da pintura, no <head>.
'use client'
import { useLayoutEffect } from 'react'

export function ThemeToggle() {
  useLayoutEffect(() => {
    const theme = localStorage.getItem('theme')
    if (theme) document.documentElement.setAttribute('data-theme', theme)
  }, [])
  // ...
}
  • O que demonstra: reaplica o atributo em desenvolvimento, porque o Strict Mode remonta e limpa atributos que o script setou (no-op em produção).

Reference Tables

SituationApproach
Data depende de request (cookies, headers)headers()/cookies() e formatar no servidor
Data atualiza ao vivo (timers, relógios)Client Component com useEffect + suppressHydrationWarning
Página já totalmente dinâmicaFormatar no servidor usando Accept-Language
Tradução entre idiomasinternationalization (builds estáticas por locale ou renderização dinâmica)

Anti-patterns

  • Formatar data direto num Client Component com toLocaleDateString(): SSR usa locale do servidor, hidratação usa o do browser, React detecta mismatch e lança erro de hidratação com flash.
  • Corrigir com useEffect: roda depois da hidratação e da pintura, então o usuário sempre vê o valor errado primeiro; também dispara re-render que pode reativar Suspense boundaries pais.
  • Ler cookie de tema no root layout com cookies(): tira a página inteira do prerendering estático (e sob Cache Components força bloqueio de todo segmento abaixo do layout).
  • CSP estrita sem nonce: dangerouslySetInnerHTML em <script> é bloqueado por Content-Security-Policy sem 'unsafe-inline'; requer nonce.

Key Takeaways

  1. O script inline roda durante o parsing do HTML, antes de React existir, então é o único jeito de evitar o flash entre "HTML chega" e "React hidrata".
  2. suppressHydrationWarning é obrigatório junto do script inline: sem ele React descarta a correção e refaz o DOM do zero.
  3. useLayoutEffect evita o flash entre hidratação e pintura, mas não o flash antes da hidratação em conexões lentas — só o inline script cobre os dois.
  4. O mesmo padrão (script inline + suppressHydrationWarning + inicializador lazy de estado) serve para datas, temas e qualquer estado de UI persistido (accordion aberto etc.).
  5. Em desenvolvimento, o Strict Mode remonta e limpa atributos do <html>/<head>/<body> que o script setou — reaplique via useLayoutEffect no componente dono.

Connects To

  • preserving-ui-state: ambos lidam com estado que precisa sobreviver a navegação/hidratação sem re-render visível.
  • internationalization: alternativa quando o problema é tradução completa, não só formatação de data/hora.