Skip to content

Commit f8a98fc

Browse files
committed
fix(apollo-vertex): use useSyncExternalStore for system theme detection
1 parent 4e7bf49 commit f8a98fc

1 file changed

Lines changed: 30 additions & 38 deletions

File tree

apps/apollo-vertex/registry/shell/shell-theme-provider.tsx

Lines changed: 30 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import type { ReactNode } from "react";
2-
import { createContext, useContext, useEffect, useState } from "react";
2+
import {
3+
createContext,
4+
useContext,
5+
useEffect,
6+
useState,
7+
useSyncExternalStore,
8+
} from "react";
39
import { THEME_STORAGE_KEY } from "./shell-constants";
410

511
export type ThemeConfig = {
@@ -65,22 +71,14 @@ function isValidTheme(value: string | null): value is Theme {
6571
return value === "light" || value === "dark" || value === "system";
6672
}
6773

68-
function getEffectiveTheme(theme: Theme): "light" | "dark" {
69-
if (theme === "system") {
70-
return window.matchMedia("(prefers-color-scheme: dark)").matches
71-
? "dark"
72-
: "light";
73-
}
74-
return theme;
75-
}
76-
77-
function applyThemeClass(theme: Theme, disableTransitions?: boolean) {
74+
function applyThemeClass(
75+
resolved: "light" | "dark",
76+
disableTransitions?: boolean,
77+
) {
7878
const root = window.document.documentElement;
79-
const resolved = getEffectiveTheme(theme);
8079

81-
let styleEl: HTMLStyleElement | null = null;
82-
if (disableTransitions) {
83-
styleEl = document.createElement("style");
80+
const styleEl = disableTransitions ? document.createElement("style") : null;
81+
if (styleEl) {
8482
styleEl.textContent =
8583
"*, *::before, *::after { transition: none !important }";
8684
document.head.append(styleEl);
@@ -96,9 +94,9 @@ function applyThemeClass(theme: Theme, disableTransitions?: boolean) {
9694
}
9795
}
9896

99-
function applyThemeConfig(config: ThemeConfig, theme: Theme) {
97+
function applyThemeConfig(config: ThemeConfig, resolved: "light" | "dark") {
10098
const root = document.documentElement;
101-
const isDark = getEffectiveTheme(theme) === "dark";
99+
const isDark = resolved === "dark";
102100

103101
for (const cssVar of Object.values(cssVarMap)) {
104102
root.style.removeProperty(cssVar);
@@ -135,34 +133,28 @@ export function ThemeProvider({
135133
return isValidTheme(stored) ? stored : defaultTheme;
136134
});
137135

138-
const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">(() =>
139-
typeof window === "undefined" ? "light" : getEffectiveTheme(theme),
136+
const systemDark = useSyncExternalStore(
137+
(cb) => {
138+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
139+
mq.addEventListener("change", cb);
140+
return () => mq.removeEventListener("change", cb);
141+
},
142+
() => window.matchMedia("(prefers-color-scheme: dark)").matches,
143+
() => false,
140144
);
141145

146+
const resolvedTheme =
147+
theme === "system" ? (systemDark ? "dark" : "light") : theme;
148+
142149
const setTheme = (newTheme: Theme) => {
143150
localStorage.setItem(storageKey, newTheme);
144151
setThemeState(newTheme);
145152
};
146153

147154
// Apply light/dark class to document root
148155
useEffect(() => {
149-
applyThemeClass(theme, disableTransitionOnChange);
150-
setResolvedTheme(getEffectiveTheme(theme));
151-
}, [theme, disableTransitionOnChange]);
152-
153-
// Listen for system theme changes when in system mode
154-
useEffect(() => {
155-
if (theme !== "system") return;
156-
157-
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
158-
const handleChange = () => {
159-
applyThemeClass(theme, disableTransitionOnChange);
160-
setResolvedTheme(getEffectiveTheme(theme));
161-
};
162-
163-
mediaQuery.addEventListener("change", handleChange);
164-
return () => mediaQuery.removeEventListener("change", handleChange);
165-
}, [theme, disableTransitionOnChange]);
156+
applyThemeClass(resolvedTheme, disableTransitionOnChange);
157+
}, [resolvedTheme, disableTransitionOnChange]);
166158

167159
// Cross-tab sync: update React state when theme changes in another tab
168160
useEffect(() => {
@@ -180,9 +172,9 @@ export function ThemeProvider({
180172
useEffect(() => {
181173
if (!themeConfig) return;
182174

183-
applyThemeConfig(themeConfig, theme);
175+
applyThemeConfig(themeConfig, resolvedTheme);
184176
return () => clearThemeConfig();
185-
}, [themeConfig, theme]);
177+
}, [themeConfig, resolvedTheme]);
186178

187179
return (
188180
<ThemeProviderContext.Provider value={{ theme, resolvedTheme, setTheme }}>

0 commit comments

Comments
 (0)