fix: derive dark-mode toggle initial state from DOM class (#1159) - #1243
Merged
hman38705 merged 1 commit intoJul 28, 2026
Conversation
…plug#1159) The blocking inline script in layout.tsx applies the correct dark/light class to <html> before first paint, but useDarkMode started from a hardcoded false default and only read the real preference in a useEffect. For dark-mode users this caused the toggle icon and aria-label to render incorrectly ('Switch to dark mode' when dark was already active) for at least one paint. Changes: - readInitialDarkMode(): new lazy useState initializer that reads document.documentElement.classList synchronously on the first render. Returns isDarkMode:true when 'dark-mode' class is present, false when 'light-mode' is present, and falls back to getDarkModePreference() if neither class was set (SSR or init script didn't run). - domClassWasApplied(): guards the mount useEffect so it skips setPreference when the DOM class was already the source of truth for the lazy init, preventing the effect from overwriting the correct state. - 5 new tests: assert isDarkMode is correct on the very first render (before isLoaded), no stale flash, correct fallback when no class is set, and toggle continues to work after lazy-init.
|
@euniceamoni Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #1159. The blocking inline script in
layout.tsxapplies the correctdark-mode/light-modeclass to<html>before first paint, butuseDarkModestarted from a hardcoded{ isDarkMode: false }default and only read the real preference inside auseEffectafter mount. For a dark-mode user this meant the toggle icon showed ☀️ ("Switch to dark mode") while the background was already dark — a stale/incorrect state for at least one paint.Root Cause
useState(defaultPreference)initializes synchronously with a hardcodedfalse, ignoring the class the init script already applied.result.currentafterrenderHookreflects state after effects, so the effect was also overwriting the correct lazy-initialized value.Fix
Two changes to
useDarkMode.ts:1. Lazy
useStateinitializer —readInitialDarkMode()readsdocument.documentElement.classListsynchronously on the first render (before any effects):dark-modeclass present →{ isDarkMode: true, hasStoredPreference: true }light-modeclass present →{ isDarkMode: false, hasStoredPreference: true }getDarkModePreference()(SSR / init script didn't run)2. Guard in the mount
useEffect—domClassWasApplied()skipssetPreferencewhen the DOM class was already the source of truth, preventing the effect from overwriting the correct state. Just marksisLoaded = true.Tests
5 new tests added:
isDarkModeistrueon the very first render whendark-modeclass is set (beforeisLoaded)isDarkModeisfalseon the first render whenlight-modeclass is setisLoadedbecomestruetoggleDarkModecontinues to work after lazy-initcd frontend && npm run build— Compiled successfully, zero warnings.Acceptance Criteria
dark-modeclass before render and asserts the toggle shows the correct initial statePR Checklist
fix/dark-mode-toggle-shows-a-stale-incorrect-icon-an)cd frontend && npm run buildpasses with zero warningscd frontend && npm test -- useDarkMode— 15 tests pass closes [Bug] Dark-mode toggle shows a stale/incorrect icon and label immediately after page load #1159