[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:
parent
777693c7dd
commit
3ee915f001
@ -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>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user