[MCH-FE] refactor: Improve ThemeContext with useSyncExternalStore

- Replace useState+useEffect with useMemo for resolvedTheme
- Use useSyncExternalStore for tracking system theme changes
- Use useLayoutEffect for DOM mutations (applying dark class)
- Add useCallback and useMemo for performance optimization

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rckrdmrd 2026-01-20 02:28:26 -06:00
parent 777693c7dd
commit 3ee915f001

View File

@ -1,4 +1,4 @@
import { createContext, useContext, useState, useEffect } from 'react';
import { createContext, useContext, useState, useLayoutEffect, useMemo, useCallback, useSyncExternalStore } from 'react';
import type { ReactNode } from 'react';
type Theme = 'light' | 'dark' | 'system';
@ -31,64 +31,61 @@ function getStoredTheme(): Theme {
return 'system';
}
// Subscribe to system theme changes
function subscribeToSystemTheme(callback: () => void) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', callback);
return () => mediaQuery.removeEventListener('change', callback);
}
function getSystemThemeSnapshot(): 'light' | 'dark' {
return getSystemTheme();
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(getStoredTheme);
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light');
// Resolve the actual theme (handles 'system' preference)
useEffect(() => {
const resolved = theme === 'system' ? getSystemTheme() : theme;
setResolvedTheme(resolved);
// Use useSyncExternalStore to track system theme changes
const systemTheme = useSyncExternalStore(
subscribeToSystemTheme,
getSystemThemeSnapshot,
() => 'light' as const // Server snapshot
);
// Apply class to document
// Compute resolved theme from theme preference and system theme
const resolvedTheme = useMemo<'light' | 'dark'>(() => {
return theme === 'system' ? systemTheme : theme;
}, [theme, systemTheme]);
// Apply class to document - useLayoutEffect is appropriate for DOM mutations
useLayoutEffect(() => {
const root = document.documentElement;
if (resolved === 'dark') {
if (resolvedTheme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
}, [theme]);
}, [resolvedTheme]);
// Listen for system theme changes
useEffect(() => {
if (theme !== 'system') return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
const newTheme = e.matches ? 'dark' : 'light';
setResolvedTheme(newTheme);
const root = document.documentElement;
if (newTheme === 'dark') {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [theme]);
const setTheme = (newTheme: Theme) => {
const setTheme = useCallback((newTheme: Theme) => {
setThemeState(newTheme);
localStorage.setItem(STORAGE_KEY, newTheme);
};
}, []);
const toggleTheme = () => {
const toggleTheme = useCallback(() => {
const newTheme = resolvedTheme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
};
}, [resolvedTheme, setTheme]);
const value = useMemo(() => ({
theme,
resolvedTheme,
setTheme,
toggleTheme,
}), [theme, resolvedTheme, setTheme, toggleTheme]);
return (
<ThemeContext.Provider
value={{
theme,
resolvedTheme,
setTheme,
toggleTheme,
}}
>
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);