Review

SSR-safe theme store via useSyncExternalStore

Studio dark-mode was reading localStorage/matchMedia inside useState+useEffect, which tore under concurrent rendering and mismatched the RSC shell on hydrate. Replace the gallery shell theme hook with useSyncExternalStore, subscribe to storage + prefers-color-scheme changes, and supply getServerSnapshot so the first paint on the server and client agree before hydration.

ReactTier 4reactssrthemeconcurrency

Click a line to flag it, pick one or more labels, then submit. If the change looks correct, approve it.

src/hooks/useGalleryTheme.ts+24-9
1-import { useEffect, useState } from "react";
1+import { useCallback, useSyncExternalStore } from "react";
22
33export type GalleryTheme = "light" | "dark";
44
55const STORAGE_KEY = "studio.gallery.theme";
66
7+function readTheme(): GalleryTheme {
8+ const stored = window.localStorage.getItem(STORAGE_KEY);
9+ if (stored === "light" || stored === "dark") return stored;
10+ return window.matchMedia("(prefers-color-scheme: dark)").matches
11+ ? "dark"
12+ : "light";
13+}
14+
15+function subscribe(onStoreChange: () => void) {
16+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
17+ window.addEventListener("storage", onStoreChange);
18+ mq.addEventListener("change", onStoreChange);
19+ return () => {
20+ window.removeEventListener("storage", onStoreChange);
21+ mq.removeEventListener("change", onStoreChange);
22+ };
23+}
24+
25+function getServerSnapshot(): GalleryTheme {
26+ return "light";
27+}
728
829export function useGalleryTheme(): GalleryTheme {
9- const [theme, setTheme] = useState<GalleryTheme>("light");
10- useEffect(() => {
11- const stored = window.localStorage.getItem(STORAGE_KEY);
12- if (stored === "light" || stored === "dark") setTheme(stored);
13- else if (window.matchMedia("(prefers-color-scheme: dark)").matches)
14- setTheme("dark");
15- }, []);
16- return theme;
30+ const getSnapshot = useCallback(() => readTheme(), []);
31+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
1732}