Fix light theme reverting to dark on full page reload
Reproduced with a headless browser: the <head> no-flash script correctly sets data-theme from localStorage before first paint (confirmed present immediately at navigation commit), but it was gone by ~50ms later and never came back — something in this app's streaming SSR/hydration does a second commit shortly after first paint that strips an attribute React doesn't know about, and suppressHydrationWarning on <html> only silences the mismatch warning for the *first* hydration diff, not a later one. Client-side navigation was unaffected (the attribute survives once already set post-mount), only a fresh full load hit this. Fix: ThemeToggle now re-applies the stored theme from localStorage in a useLayoutEffect on mount, which runs after the attribute-stripping commit has already happened and reliably makes it stick. Verified with the same repro: data-theme now stays "light" at commit, +50ms, +550ms, and +2550ms after reload, both directly and through the original toggle -> navigate -> reload sequence. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY
This commit is contained in:
1 parent
71c0689af9
commit
1dcf5a15a5
1 file changed
+17
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useLayoutEffect } from "react";
|
||||
import { Sun, Moon } from "lucide-react";
|
||||
|
||||
function getEffectiveTheme(): "light" | "dark" {
|
||||
@@ -12,6 +13,22 @@ function getEffectiveTheme(): "light" | "dark" {
|
||||
// palette itself uses) decides which one shows — avoids a useState/useEffect
|
||||
// pair just to reflect the DOM attribute the no-flash script already set.
|
||||
export function ThemeToggle() {
|
||||
// The <head> no-flash script sets data-theme before hydration, but
|
||||
// something in this app's streaming SSR/hydration commits again shortly
|
||||
// after first paint and strips it back off (confirmed: present at ~0ms,
|
||||
// gone by ~50ms, and it doesn't come back on its own) — suppressHydration
|
||||
// Warning on <html> only silences the mismatch warning for the *first*
|
||||
// hydration diff, it doesn't stop a later commit from resetting an
|
||||
// attribute React doesn't know about. Re-applying here, once this client
|
||||
// component has mounted (i.e. after that reset has already happened),
|
||||
// wins the race and makes it stick.
|
||||
useLayoutEffect(() => {
|
||||
const stored = localStorage.getItem("theme");
|
||||
if (stored === "light" || stored === "dark") {
|
||||
document.documentElement.dataset.theme = stored;
|
||||
}
|
||||
}, []);
|
||||
|
||||
function toggle() {
|
||||
const next = getEffectiveTheme() === "dark" ? "light" : "dark";
|
||||
document.documentElement.dataset.theme = next;
|
||||
|
||||
Reference in new issue
Block a user