Skip to content

Commit 4ec046a

Browse files
committed
🤖 refactor: simplify Storybook theme injection and remove race conditions
1 parent e065961 commit 4ec046a

File tree

1 file changed

+27
-43
lines changed

1 file changed

+27
-43
lines changed

.storybook/preview.tsx

Lines changed: 27 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import React from "react";
22
import type { Preview } from "@storybook/react-vite";
3-
import { ThemeProvider, type ThemeMode } from "../src/browser/contexts/ThemeContext";
4-
import { getStorageChangeEvent } from "@/common/constants/events";
3+
import {
4+
ThemeProvider,
5+
type ThemeMode,
6+
useTheme,
7+
} from "../src/browser/contexts/ThemeContext";
58
import isChromatic from "chromatic/isChromatic";
69
import { UI_THEME_KEY } from "@/common/constants/storage";
10+
import { updatePersistedState } from "@/browser/hooks/usePersistedState";
711
import "../src/browser/styles/globals.css";
812

13+
const DEFAULT_THEME: ThemeMode = "light";
14+
915
const isAutomatedChromaticRun = () => {
1016
if (typeof window === "undefined" || !isChromatic()) {
1117
return false;
@@ -30,65 +36,45 @@ const isE2ETestEnv = () => {
3036
return false;
3137
};
3238

33-
const applyStorybookTheme = (mode: ThemeMode) => {
39+
const getPersistedTheme = (): ThemeMode | undefined => {
3440
if (typeof window === "undefined") {
35-
return;
41+
return undefined;
3642
}
3743

3844
try {
39-
const serialized = JSON.stringify(mode);
40-
const prev = window.localStorage.getItem(UI_THEME_KEY);
41-
const root = document.documentElement;
42-
const current = root?.dataset?.theme as ThemeMode | undefined;
43-
if (prev === serialized && current === mode) {
44-
return; // no-op if already applied
45-
}
46-
47-
window.localStorage.setItem(UI_THEME_KEY, serialized);
48-
root.dataset.theme = mode;
49-
root.style.colorScheme = mode;
50-
51-
const event = new CustomEvent(getStorageChangeEvent(UI_THEME_KEY), {
52-
detail: { key: UI_THEME_KEY, newValue: mode, origin: "storybook" },
53-
});
54-
window.dispatchEvent(event);
45+
const stored = window.localStorage.getItem(UI_THEME_KEY);
46+
return stored ? (JSON.parse(stored) as ThemeMode) : undefined;
5547
} catch (error) {
56-
console.warn("Failed to write Storybook theme:", error);
48+
console.warn("Failed to read Storybook theme:", error);
49+
return undefined;
5750
}
5851
};
5952

6053
const syncStorybookTheme = (mode?: ThemeMode): ThemeMode => {
6154
if (typeof window === "undefined") {
62-
return mode ?? "light"; // Keep default aligned with baseline to avoid Chromatic diffs
55+
return mode ?? DEFAULT_THEME; // Keep default aligned with baseline to avoid Chromatic diffs
6356
}
6457

65-
const stored = window.localStorage.getItem(UI_THEME_KEY);
66-
const persisted = stored ? (JSON.parse(stored) as ThemeMode) : undefined;
67-
const resolved = mode ?? persisted ?? "light"; // Default to light for Latest when unset
68-
applyStorybookTheme(resolved);
69-
return resolved;
70-
};
58+
const persisted = getPersistedTheme();
59+
const resolved = mode ?? persisted ?? DEFAULT_THEME; // Default to light for Latest when unset
7160

72-
const StorybookThemeToggle: React.FC<{ initialTheme: ThemeMode }> = ({ initialTheme }) => {
73-
const [theme, setTheme] = React.useState(initialTheme);
61+
if (resolved !== persisted) {
62+
updatePersistedState(UI_THEME_KEY, resolved);
63+
}
7464

75-
React.useEffect(() => {
76-
setTheme(initialTheme);
77-
}, [initialTheme]);
65+
return resolved;
66+
};
7867

79-
const handleToggle = () => {
80-
const next = theme === "dark" ? "light" : "dark";
81-
applyStorybookTheme(next);
82-
setTheme(next);
83-
};
68+
const StorybookThemeToggle: React.FC = () => {
69+
const { theme, toggleTheme } = useTheme();
8470

8571
if (typeof window === "undefined") {
8672
return null;
8773
}
8874

8975
return (
9076
<button
91-
onClick={handleToggle}
77+
onClick={toggleTheme}
9278
style={{
9379
position: "fixed",
9480
bottom: "1rem",
@@ -128,13 +114,11 @@ const preview: Preview = {
128114
decorators: [
129115
(Story, context) => {
130116
const mode = context.globals.theme as ThemeMode | undefined;
131-
const resolved = syncStorybookTheme(mode);
117+
syncStorybookTheme(mode);
132118
return (
133119
<ThemeProvider>
134120
<Story />
135-
{!mode && !isE2ETestEnv() && (
136-
<StorybookThemeToggle initialTheme={resolved} />
137-
)}
121+
{!mode && !isE2ETestEnv() && <StorybookThemeToggle />}
138122
</ThemeProvider>
139123
);
140124
},

0 commit comments

Comments
 (0)