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
This issue is because of
localStorage
inpresetTheme()
.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
anduseEffect
hooks, which are executed only during client render.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.
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: