skip to Main Content

I’ve earlier used _document.tsx to handle some script logic that needed to be very early. E.g. I might have dark mode code there to prevent flickering which might occur if doing useEffect.

For example my _document.tsx might be:

import Document, { Head, Main, NextScript, Html } from "next/document";
import React from "react";

function presetTheme() {
  const dark = localStorage.getItem("theme") === "dark";

  if (dark) {
    document.body.classList.add("dark");
  }
}

const themeScript = `(() => { ${presetTheme.toString()}; presetTheme() })()`;

class MyDocument extends Document {
  render() {
    return (
      <Html lang={lang}>
        <Head />
        <body>
          <script dangerouslySetInnerHTML={{ __html: themeScript, }} />
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

I now need similar functionality using Next 13.4+ with App Router and layout.tsx, still to run it early enough, but also to avoid/handle a warning I’m receiving.

For example my layout.tsx might be:

import "../styles/global.css";

function presetTheme() {
  const dark = localStorage.getItem("theme") === "dark";

  if (dark) {
      document.body.classList.add("dark");
  }
}

const themeScript = `(() => { ${presetTheme.toString()}; presetTheme() })()`;

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <script dangerouslySetInnerHTML={{ __html: themeScript, }} />
        <div className="mx-auto px-4">
          {children}
        </div>
      </body>
    </html>
  );
}

Running with this layout.tsx gives me the following warning:

Warning: Extra attributes from the server: class
    at body
    at html
    at RedirectErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/redirect-boundary.js:73:9)
    at RedirectBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/redirect-boundary.js:81:11)
    at NotFoundErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/not-found-boundary.js:51:9)
    at NotFoundBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/not-found-boundary.js:59:11)
    at ReactDevOverlay (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:66:9)
    at HotReload (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:276:11)
    at Router (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/app-router.js:90:11)
    at ErrorBoundaryHandler (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/error-boundary.js:80:9)
    at ErrorBoundary (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/error-boundary.js:106:11)
    at AppRouter (webpack-internal:///(app-client)/./node_modules/next/dist/client/components/app-router.js:374:13)
    at ServerRoot (webpack-internal:///(app-client)/./node_modules/next/dist/client/app-index.js:154:11)
    at RSCComponent
    at Root (webpack-internal:///(app-client)/./node_modules/next/dist/client/app-index.js:171:11)

The error I’m understanding occurs due to me adding class="dark" with the script that I run early to prevent dark mode flickering.

A couple of points of interest for me regarding this warning:

  • Can I handle it more properly, to avoid the scenario?
  • Is this a warning I can safely ignore taking my usage into account?
  • Does the warning indicate any kind of performance impact?

2

Answers


  1. This issue is because of localStorage in presetTheme().

    While rendering your application, there was a difference between the React tree that was pre-rendered from the server and the React tree that was rendered during the first render in the browser (hydration).

    https://nextjs.org/docs/messages/react-hydration-error

    localStorage.getItem("theme") will be different on server render vs client render. Hence, Next.js reports this error.

    You can solve this by using useState and useEffect hooks, which are executed only during client render.

    import "../styles/global.css";
    
    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      const [themeScript, setThemeScript] = useState('')
     
      useEffect(() => {
        const presetTheme = () => {
          const dark = localStorage.getItem("theme") === "dark";
    
          if (dark) {
            document.body.classList.add("dark");
          }
        }
    
        setThemeScript(`(() => { ${presetTheme.toString()}; presetTheme() })()`)
      }, [])
    
      return (
        <html lang="en">
          <body>
            <script dangerouslySetInnerHTML={{ __html: themeScript, }} />
            <div className="mx-auto px-4">
              {children}
            </div>
          </body>
        </html>
      );
    }
    
    Login or Signup to reply.
  2. To solve your flickering problem, you need to have the state set in the server. Because, as long as you have the state set in the client, the client needs to download the script first, and then change the theme, leading to the flickering problem.

    One way to solve this is to use cookies.

    The cookies function allows you to read the HTTP incoming request cookies from a Server Component

    https://nextjs.org/docs/app/api-reference/functions/cookies

    Which means, rather than storing the theme in localStorage, you store it in a cookie.

    You then need to modify your _document.tsx to read the theme from the cookie, like shown below:

    import Document, { Head, Main, NextScript, Html } from "next/document";
    import { cookies } from 'next/headers'
    import React from "react";
    
    class MyDocument extends Document {
      const dark = cookies().get('theme') === 'dark'
    
      render() {
        return (
          <Html lang="en">
            <Head />
            <body className={dark ? "dark" : ""}>
              <Main />
              <NextScript />
            </body>
          </Html>
        );
      }
    }
    
    export default MyDocument;
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search