Missing `.dark` class when `notFound()` is invoked
Link to the code that reproduces this issue
https://github.com/loqusion/next-bug-class-removed
To Reproduce
- Start the application with
pnpm run build && pnpm run start(bug can also be reproduced in development) - Navigate to
localhost:3000/notfound(page invokesnotFound()) - Open developer tools to inspect HTML
<html>tag does not have.darkclass
Control group
- Navigate to
localhost:3000/any(non-existent page) - In production,
<html>tag hasclass="dark"as expected
Current vs. Expected behavior
A <script> tag containing document.documentElement.classList.add('dark') is present in <head>, which should update the root document tag (<html>) to have the .dark class. This works on most pages, but not on pages where notFound() is called, even though the <script> is still present. (In the development build, any "404 not found" page also exhibits this bug.)
Verify canary release
- [x] I verified that the issue exists in the latest Next.js canary release
Provide environment information
Operating System:
Platform: linux
Arch: x64
Version: #1 ZEN SMP PREEMPT_DYNAMIC Mon, 04 Dec 2023 00:28:58 +0000
Binaries:
Node: 21.4.0
npm: 10.2.5
Yarn: 1.22.21
pnpm: 8.12.0
Relevant Packages:
next: 14.0.5-canary.4
eslint-config-next: N/A
react: 18.2.0
react-dom: 18.2.0
typescript: 5.1.3
Next.js Config:
output: N/A
Which area(s) are affected? (Select all that apply)
App Router
Additional context
The reproduction example is not very useful for real life applications, but the same bug occurs for a theme hydration script that is used to prevent theme flicker on initial page load:
try {
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
document.querySelector('meta[name="theme-color"]').setAttribute('content', '#0B1120')
} else {
document.documentElement.classList.remove('dark')
}
} catch (_) {}
The script is from tailwindcss.com (source). The bug does not appear on the site (possibly because it uses the pages router, or because it uses an older version of Next.js, or because it never invokes notFound()). However, the bug does occur on my own site where I use the same script (I use app router).
I've reproduced the issue locally, and via Vercel deployment, on both Firefox and Chromium.
- The browser API call should only be in useEffect
- You're obviously doing shit.
@maximlambov
- The browser API call should only be in useEffect
- You're obviously doing shit.
- The browser API call is being made in a
<script>tag, not in the render function of a React client component. It needs to be done this way to avoid screen flicker, which you would know if you had read what I wrote. - That was uncalled for.
The markup's state created by inlined scripts is not a part of NextJS server/client component tree, so it can easily be lost on any change. I guess the closest thing to a solution here is using router.replace("/404") to retain the state of layout, assuming notFound() in there is supposed to emulate client-side transition and therefore you don't care about the status code of this "redirect".
For this issue, I've found a couple of workarounds, each of which has its own trade-offs:
router.replace('/404')(fromuseRouter()) (thanks @GabenGar)- Has
404status, but doesn't retain browser URL - Must be done in a client component, and requires client JavaScript
- Has
return <NotFound />(imported from customnot-foundcomponent)- Retains the browser URL, but doesn't have
404status - Works without client JavaScript
- Retains nested layout UI, which may or may not be what you want
- If done in a descendant of the main
page.jscomponent, all of the otherpage.jsUI will also be retained
- If done in a descendant of the main
- Retains the browser URL, but doesn't have
notFound()(for comparison)- Has
404status, and retains the browser URL - Doesn't need to be called directly in a React component
- Triggers Error Boundaries
- The
.darkclass also disappears in error boundaries after triggeringnotFound()
- The
- Can be worked around with an additional
<Script>(withstrategy="afterInteractive") that does backup theme hydration, though this can cause theme flicker on pages where the.darkclass disappears and I'm not sure how consistent this workaround is
- Has
It would be nice if there were an officially supported way to do theme hydration, though I'm not sure how much development effort that would require or if it would be worth it.
This issue has been automatically marked as stale due to two years of inactivity. It will be closed in 7 days unless there’s further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you.
This issue has been automatically closed due to two years of inactivity. If you’re still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding!
This closed issue has been automatically locked because it had no new activity for 2 weeks. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.