Theme before hydration

Set stored preferences in a tiny inline script before React hydrates.

From vercel-react-best-practices: rendering-hydration-no-flicker and 1 more.

localStorage.theme

Reload in dark mode

Press Reload, then try a slower hydration delay.

Read in useEffectBad

    useState('light') + useEffect
    Inline script firstGood

      <script>{themeScript}</script>
      function ThemeWrapper({ children }: { children: ReactNode }) {
        return (
          <>
            <div id="theme-wrapper">{children}</div>
            <script
              dangerouslySetInnerHTML={{
                __html: `(function() {
                  try {
                    var theme = localStorage.getItem('theme') || 'light';
                    var el = document.getElementById('theme-wrapper');
                    if (el) el.className = theme;
                  } catch (e) {}
                })();`,
              }}
            />
          </>
        )
      }

      Server render, then hydrate

      The server has no localStorage.

      Read in renderBad
      Not rendered
        const theme = localStorage.getItem('theme')
        typeof window guardBad
        Not rendered
          typeof window === 'undefined' ? 'light' : …
          Inline scriptGood
          Not rendered
            <div id="theme-wrapper" suppressHydrationWarning>
            <div id="theme-wrapper" suppressHydrationWarning>{children}</div>
            <script dangerouslySetInnerHTML={{ __html: themeScript }} />

            Silence only expected diffs

            Pick a time zone and cart count, then Hydrate.

            Timestamp, no flagNoisy
            Rendered at—
              <span>{date.toLocaleString()}</span>
              Timestamp, flaggedGood
              Rendered at—
                <span suppressHydrationWarning>
                Cart count, flaggedHidden bug
                Cart—
                  <span suppressHydrationWarning>{cart.length}
                  function Timestamp() {
                    return (
                      <span suppressHydrationWarning>
                        {new Date().toLocaleString()}
                      </span>
                    )
                  }

                  Why it matters, pros and cons

                  Why it matters

                  The server can't read localStorage. An inline script fixes the first frame.

                  Pros

                  • No wrong first frame, however slow JS is
                  • Server output stays deterministic
                  • A few hundred bytes, no dependency

                  Cons

                  • Inline scripts need a CSP nonce or hash
                  • If a cookie can tell the server, prefer that
                  • suppressHydrationWarning is one level deep; it hides real bugs