Skip to content

fix(studio): avoid shell theme hydration mismatch on SSR#106

Merged
iipanda merged 2 commits intomainfrom
fix/studio-shell-theme-hydration-mismatch
Apr 17, 2026
Merged

fix(studio): avoid shell theme hydration mismatch on SSR#106
iipanda merged 2 commits intomainfrom
fix/studio-shell-theme-hydration-mismatch

Conversation

@iipanda
Copy link
Copy Markdown
Collaborator

@iipanda iipanda commented Apr 17, 2026

Summary

Two-part fix for the studio shell theme hydration.

1. Hydration mismatch (first commit). useResolvedShellTheme read window.localStorage / window.matchMedia inside its useState initializer. SSR produced data-mdcms-theme="light" while the first client render produced "dark", so React logged a hydration mismatch on the shell root and refused to patch the attribute. The hook now initializes to a stable "light" so SSR and first client render agree.

2. Dark-theme flash (second commit). Fix #1 alone leaves dark-mode users looking at light styles until useEffect runs post-paint. We now:

  • Emit an inline <script> as the first child of the shell root that reads the stored preference and system media query synchronously during HTML parsing and mutates data-mdcms-theme before the browser paints (the same pattern next-themes uses for <html>, scoped to the shell).
  • Add suppressHydrationWarning on the root so React tolerates the pre-hydration DOM mutation.
  • Use an isomorphic layout effect (useLayoutEffect on client, useEffect fallback for SSR) so client-only mounts — e.g., SPA navigation into Studio — also resolve the theme before paint rather than after.

No regression for explicit shellTheme prop callers (tests asserting data-mdcms-theme="light" / "dark" still pass).

Test plan

  • bun test --cwd packages/studio ./src/lib/studio.test.ts (38 pass, including new inline-script assertion)
  • bunx nx typecheck studio
  • Load Studio route with prefers-color-scheme: dark and no stored preference: confirm no hydration error and no white flash on initial load
  • Same with stored "light" preference while system is dark: confirm light theme renders without flicker
  • Toggle system theme at runtime: shell updates live

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • More reliable theme initialization on startup to avoid incorrect theme flashes.
  • Refactor

    • Theme syncing now runs earlier in the render lifecycle to apply the correct theme sooner.
  • New Features

    • Injects an early inline theme initializer to align DOM theme before hydration.
  • Tests

    • Added a unit test confirming the pre-hydration theme initialization script is rendered.

useResolvedShellTheme initialized state by reading window.localStorage
and window.matchMedia, so SSR emitted data-mdcms-theme="light" while
the first client render resolved to the user's actual preference,
triggering a React hydration mismatch on the studio shell root. Start
with a stable "light" value on both server and first client render and
defer resolution to useEffect so the real theme is applied post-mount.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 17, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c900d40-164b-4709-bb7a-222e7700b169

📥 Commits

Reviewing files that changed from the base of the PR and between 756fbaf and c260c7d.

📒 Files selected for processing (2)
  • packages/studio/src/lib/studio-component.tsx
  • packages/studio/src/lib/studio.test.ts

📝 Walkthrough

Walkthrough

Switches theme initialization to a constant "light" on first render, introduces useIsomorphicLayoutEffect (uses useLayoutEffect in browser / useEffect on server), moves client theme resolution into that effect for earlier syncing, adds an inline theme bootstrap script export and injects it into StudioShellFrame with suppressHydrationWarning.

Changes

Cohort / File(s) Summary
Studio UI & theme runtime
packages/studio/src/lib/studio-component.tsx
Add useIsomorphicLayoutEffect; change useResolvedShellTheme initial state to constant "light"; move theme recomputation into isomorphic layout effect; avoid redundant state updates; export SHELL_THEME_INLINE_SCRIPT; inject inline script via dangerouslySetInnerHTML and set suppressHydrationWarning on root element.
Unit test
packages/studio/src/lib/studio.test.ts
Add test asserting StudioShellFrame renders a pre-hydration inline script (IIFE) referencing mdcms-studio-theme, prefers-color-scheme: dark branch, and a call to setAttribute("data-mdcms-theme") when startupState is "loading" and no shellTheme prop provided.

Sequence Diagram(s)

sequenceDiagram
  participant Server as Server (SSR)
  participant HTML as Initial HTML
  participant Browser as Browser (pre-hydration)
  participant Shell as StudioShellFrame
  participant Effect as useIsomorphicLayoutEffect
  Server->>HTML: render initial markup + inline SHELL_THEME_INLINE_SCRIPT
  HTML->>Browser: deliver HTML
  Browser->>Shell: mount StudioShellFrame (suppressHydrationWarning)
  Shell->>Effect: run isomorphic layout effect (early)
  Effect->>Browser: read localStorage (`STUDIO_THEME_STORAGE_KEY`) and matchMedia
  Effect->>Shell: setAttribute("data-mdcms-theme", resolvedTheme) / avoid no-op updates
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 Soft paws tap keys at break of day,
I set the theme so light can stay,
A tiny script whispers, "Choose your glow,"
Before the browser wakes the show —
Hop, hydrate, and let colors play ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main fix in the changeset: preventing shell theme hydration mismatch during server-side rendering by deferring theme resolution and injecting an inline script.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/studio-shell-theme-hydration-mismatch

Comment @coderabbitai help to get the list of available commands and usage tips.

…dration

Initializing useResolvedShellTheme to a stable "light" on SSR avoided
the hydration mismatch but left dark-mode users with a brief light-theme
flash until useEffect ran post-mount. Inject an inline script as the
first child of the shell root that reads the stored preference and
system media query synchronously during HTML parsing, mutating
data-mdcms-theme before the browser paints. suppressHydrationWarning
lets React tolerate the pre-hydration mutation, and the hook now uses
an isomorphic layout effect so client-only mounts also resolve the
theme before paint instead of after. Adds a test asserting the inline
script is emitted in SSR markup.
@iipanda iipanda merged commit de11ac3 into main Apr 17, 2026
4 of 5 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.

1 participant