Skip to content

fix: derive dark-mode toggle initial state from DOM class (#1159) - #1243

Merged
hman38705 merged 1 commit into
solutions-plug:mainfrom
euniceamoni:fix/dark-mode-toggle-shows-a-stale-incorrect-icon-an
Jul 28, 2026
Merged

fix: derive dark-mode toggle initial state from DOM class (#1159)#1243
hman38705 merged 1 commit into
solutions-plug:mainfrom
euniceamoni:fix/dark-mode-toggle-shows-a-stale-incorrect-icon-an

Conversation

@euniceamoni

@euniceamoni euniceamoni commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #1159. The blocking inline script in layout.tsx applies the correct dark-mode/light-mode class to <html> before first paint, but useDarkMode started from a hardcoded { isDarkMode: false } default and only read the real preference inside a useEffect after 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 hardcoded false, ignoring the class the init script already applied. result.current after renderHook reflects state after effects, so the effect was also overwriting the correct lazy-initialized value.

Fix

Two changes to useDarkMode.ts:

1. Lazy useState initializerreadInitialDarkMode() reads document.documentElement.classList synchronously on the first render (before any effects):

  • dark-mode class present → { isDarkMode: true, hasStoredPreference: true }
  • light-mode class present → { isDarkMode: false, hasStoredPreference: true }
  • Neither class → falls back to getDarkModePreference() (SSR / init script didn't run)

2. Guard in the mount useEffectdomClassWasApplied() skips setPreference when the DOM class was already the source of truth, preventing the effect from overwriting the correct state. Just marks isLoaded = true.

Tests

5 new tests added:

  • isDarkMode is true on the very first render when dark-mode class is set (before isLoaded)
  • isDarkMode is false on the first render when light-mode class is set
  • No stale flash: state is correct both before and after isLoaded becomes true
  • Falls back correctly when neither class is set
  • toggleDarkMode continues to work after lazy-init
Tests: 15 passed, 15 total

cd frontend && npm run build — Compiled successfully, zero warnings.

Acceptance Criteria

  • Toggle icon/label matches the actually-applied theme on first render, no flash of incorrect state
  • Toggling between modes continues to work correctly after the fix
  • A test sets the dark-mode class before render and asserts the toggle shows the correct initial state

PR Checklist

…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.
@drips-wave

drips-wave Bot commented Jul 27, 2026

Copy link
Copy Markdown

@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! 🚀

Learn more about application limits

@hman38705
hman38705 merged commit 6b6893f into solutions-plug:main Jul 28, 2026
6 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Dark-mode toggle shows a stale/incorrect icon and label immediately after page load

2 participants