Skip to content

Commit a239495

Browse files
committed
🤖 fix(storybook): remove theme sync race condition
Removed duplicate theme application logic that was causing conflicts between the preview decorator and ThemeProvider. Previously, both `syncStorybookTheme()` and `ThemeProvider`'s `applyThemeToDocument()` were trying to control document.documentElement, causing race conditions where some light mode stories would render as dark (e.g., ErrorMessage Long message). Changes: - Removed `applyStorybookTheme()` and `syncStorybookTheme()` functions - Simplified decorator to only pass forcedTheme to ThemeProvider - ThemeProvider now disables persistence listener when forcedTheme is set - Toggle is disabled when theme is forced (prevents conflicts) This makes ThemeProvider the single source of truth for theme application, eliminating timing issues and localStorage conflicts. Net: -40 lines
1 parent dd40c7e commit a239495

File tree

2 files changed

+6
-45
lines changed

2 files changed

+6
-45
lines changed

.storybook/preview.tsx

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,8 @@
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 { getStorageChangeEvent } from "@/common/constants/events";
5-
import { UI_THEME_KEY } from "@/common/constants/storage";
64
import "../src/browser/styles/globals.css";
75

8-
const applyStorybookTheme = (mode: ThemeMode) => {
9-
if (typeof window === "undefined") {
10-
return;
11-
}
12-
13-
try {
14-
const serialized = JSON.stringify(mode);
15-
const prev = window.localStorage.getItem(UI_THEME_KEY);
16-
const root = document.documentElement;
17-
const current = root?.dataset?.theme as ThemeMode | undefined;
18-
if (prev === serialized && current === mode) {
19-
return; // no-op if already applied
20-
}
21-
22-
window.localStorage.setItem(UI_THEME_KEY, serialized);
23-
root.dataset.theme = mode;
24-
root.style.colorScheme = mode;
25-
26-
const event = new CustomEvent(getStorageChangeEvent(UI_THEME_KEY), {
27-
detail: { key: UI_THEME_KEY, newValue: mode, origin: "storybook" },
28-
});
29-
window.dispatchEvent(event);
30-
} catch (error) {
31-
console.warn("Failed to write Storybook theme:", error);
32-
}
33-
};
34-
35-
const syncStorybookTheme = (mode?: ThemeMode): ThemeMode => {
36-
if (typeof window === "undefined") {
37-
return mode ?? "dark"; // Default to dark theme
38-
}
39-
40-
const stored = window.localStorage.getItem(UI_THEME_KEY);
41-
const persisted = stored ? (JSON.parse(stored) as ThemeMode) : undefined;
42-
const resolved = mode ?? persisted ?? "dark"; // Default to dark for Latest when unset
43-
applyStorybookTheme(resolved);
44-
return resolved;
45-
};
46-
476
const preview: Preview = {
487
globalTypes: {
498
theme: {
@@ -62,7 +21,6 @@ const preview: Preview = {
6221
decorators: [
6322
(Story, context) => {
6423
const mode = context.globals.theme as ThemeMode | undefined;
65-
syncStorybookTheme(mode);
6624
return (
6725
<ThemeProvider forcedTheme={mode}>
6826
<Story />

src/browser/contexts/ThemeContext.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function applyThemeToDocument(theme: ThemeMode) {
5353

5454
export function ThemeProvider(props: { children: ReactNode; forcedTheme?: ThemeMode }) {
5555
const [theme, setTheme] = usePersistedState<ThemeMode>(UI_THEME_KEY, resolveSystemTheme(), {
56-
listener: true,
56+
listener: !props.forcedTheme, // Disable listener when theme is forced (avoids conflicts)
5757
});
5858

5959
// Use forcedTheme if provided (e.g., from Storybook), otherwise use persisted theme
@@ -64,8 +64,11 @@ export function ThemeProvider(props: { children: ReactNode; forcedTheme?: ThemeM
6464
}, [activeTheme]);
6565

6666
const toggleTheme = useCallback(() => {
67-
setTheme((current) => (current === "dark" ? "light" : "dark"));
68-
}, [setTheme]);
67+
// Only allow toggling when not forced
68+
if (!props.forcedTheme) {
69+
setTheme((current) => (current === "dark" ? "light" : "dark"));
70+
}
71+
}, [props.forcedTheme, setTheme]);
6972

7073
const value = useMemo<ThemeContextValue>(
7174
() => ({

0 commit comments

Comments
 (0)