Skip to content

Commit d32f711

Browse files
committed
🤖 fix: apply Storybook theme synchronously before React renders
Root cause: Original code used useEffect to set theme, which is async. Chromatic sometimes captured snapshots before the effect ran. Also removed: - preview-head.html (not needed, decorator handles theme) - ThemeProvider.forcedTheme prop (not needed, decorator sets localStorage) Fix: - Restored defaultValue: 'dark' in globalTypes (ensures context.globals.theme always has value) - Apply theme synchronously in decorator BEFORE React renders - Write to localStorage, document.dataset.theme, and colorScheme directly Net: -25 lines
1 parent f248522 commit d32f711

File tree

3 files changed

+20
-45
lines changed

3 files changed

+20
-45
lines changed

.storybook/preview-head.html

Lines changed: 0 additions & 16 deletions
This file was deleted.

.storybook/preview.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import React from "react";
22
import type { Preview } from "@storybook/react-vite";
33
import { ThemeProvider, type ThemeMode } from "../src/browser/contexts/ThemeContext";
4+
import { UI_THEME_KEY } from "../src/common/constants/storage";
45
import "../src/browser/styles/globals.css";
56

67
const preview: Preview = {
78
globalTypes: {
89
theme: {
910
name: "Theme",
1011
description: "Choose between light and dark UI themes",
12+
defaultValue: "dark",
1113
toolbar: {
1214
icon: "mirror",
1315
items: [
@@ -20,17 +22,19 @@ const preview: Preview = {
2022
},
2123
decorators: [
2224
(Story, context) => {
23-
const mode = context.globals.theme as ThemeMode | undefined;
25+
const mode = (context.globals.theme ?? "dark") as ThemeMode;
2426

25-
// Apply theme synchronously to document BEFORE React renders
26-
// This ensures Chromatic snapshots capture the correct theme without flashing
27-
if (mode && typeof document !== "undefined") {
27+
// Apply theme synchronously BEFORE React renders
28+
// This ensures Chromatic snapshots capture the correct theme without timing issues
29+
if (typeof window !== "undefined") {
30+
const serialized = JSON.stringify(mode);
31+
window.localStorage.setItem(UI_THEME_KEY, serialized);
2832
document.documentElement.dataset.theme = mode;
2933
document.documentElement.style.colorScheme = mode;
3034
}
3135

3236
return (
33-
<ThemeProvider forcedTheme={mode}>
37+
<ThemeProvider>
3438
<Story />
3539
</ThemeProvider>
3640
);

src/browser/contexts/ThemeContext.tsx

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,39 +51,26 @@ function applyThemeToDocument(theme: ThemeMode) {
5151
}
5252
}
5353

54-
export function ThemeProvider(props: { children: ReactNode; forcedTheme?: ThemeMode }) {
55-
// Only use persistence when theme is NOT forced
56-
// When forcedTheme is provided (e.g., from Chromatic modes), bypass persistence entirely
57-
const [persistedTheme, setPersistedTheme] = usePersistedState<ThemeMode>(
58-
UI_THEME_KEY,
59-
resolveSystemTheme(),
60-
{
61-
listener: !props.forcedTheme, // Disable listener when theme is forced (avoids conflicts)
62-
}
63-
);
64-
65-
// Use forcedTheme if provided, otherwise use persisted theme
66-
// This ensures Chromatic modes (which set forcedTheme) are respected
67-
const activeTheme = props.forcedTheme ?? persistedTheme;
54+
export function ThemeProvider(props: { children: ReactNode }) {
55+
const [theme, setTheme] = usePersistedState<ThemeMode>(UI_THEME_KEY, resolveSystemTheme(), {
56+
listener: true,
57+
});
6858

6959
useLayoutEffect(() => {
70-
applyThemeToDocument(activeTheme);
71-
}, [activeTheme]);
60+
applyThemeToDocument(theme);
61+
}, [theme]);
7262

7363
const toggleTheme = useCallback(() => {
74-
// Only allow toggling when not forced
75-
if (!props.forcedTheme) {
76-
setPersistedTheme((current) => (current === "dark" ? "light" : "dark"));
77-
}
78-
}, [props.forcedTheme, setPersistedTheme]);
64+
setTheme((current) => (current === "dark" ? "light" : "dark"));
65+
}, [setTheme]);
7966

8067
const value = useMemo<ThemeContextValue>(
8168
() => ({
82-
theme: activeTheme,
83-
setTheme: setPersistedTheme,
69+
theme,
70+
setTheme,
8471
toggleTheme,
8572
}),
86-
[activeTheme, setPersistedTheme, toggleTheme]
73+
[setTheme, theme, toggleTheme]
8774
);
8875

8976
return <ThemeContext.Provider value={value}>{props.children}</ThemeContext.Provider>;

0 commit comments

Comments
 (0)