←

Inline script en layout.tsx para evitar flash de tema incorrecto

Contexto

Al implementar dark mode en un portfolio Next.js 16, el sitio flasheaba blanco por una fraccion de segundo antes de aplicar el tema guardado en localStorage. El problema: React hydration ocurre despues de que el browser ya pinto el primer frame.

Lo que aprendi

La solucion es inyectar un <script> sincrono directamente en <head> usando dangerouslySetInnerHTML. Este script se ejecuta antes de que el browser pinte, asi que el usuario nunca ve el flash.

El truco clave es que este script NO puede ser un componente React ni usar next/script con strategy="afterInteractive" -- esos se ejecutan demasiado tarde. Tiene que ser inline y sincrono.

// app/layout.tsx
<html lang="es" className={fontVariables} suppressHydrationWarning>
  <head>
    <script
      dangerouslySetInnerHTML={{
        __html: `(function(){
          var t = localStorage.getItem("site-theme") || "editorial";
          var m = localStorage.getItem("site-mode") || "dark";
          document.documentElement.classList.add("theme-" + t);
          if (m === "dark") document.documentElement.classList.add("dark");
        })()`,
      }}
    />
  </head>

Puntos importantes:

  • suppressHydrationWarning en <html> es necesario porque el server no sabe que clases agrego el script, y React se quejaria del mismatch
  • El script lee de localStorage (que es sincrono) y agrega clases al documentElement
  • Se envuelve en IIFE para no contaminar el scope global
  • El default es "dark" porque es el modo preferido del sitio

Por que no usar next/script

next/script con strategy="beforeInteractive" suena como la solucion, pero en App Router tiene limitaciones. El approach mas confiable sigue siendo dangerouslySetInnerHTML directo en <head> dentro del layout.

Referencia