From 29cc1392405283a0015aca318b1d06af5757c807 Mon Sep 17 00:00:00 2001 From: Un7corn Date: Thu, 12 Mar 2026 11:58:33 +0800 Subject: [PATCH] Create useDarkMode.ts --- .../template/src/hooks/useDarkMode.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 packages/cra-template-typescript/template/src/hooks/useDarkMode.ts diff --git a/packages/cra-template-typescript/template/src/hooks/useDarkMode.ts b/packages/cra-template-typescript/template/src/hooks/useDarkMode.ts new file mode 100644 index 0000000000..872b1e4ad5 --- /dev/null +++ b/packages/cra-template-typescript/template/src/hooks/useDarkMode.ts @@ -0,0 +1,78 @@ +import { useEffect, useState } from 'react' + +type Theme = 'light' | 'dark' + +const STORAGE_KEY = 'theme' +const MEDIA_QUERY = '(prefers-color-scheme: dark)' + +function getStoredTheme(): Theme | null { + try { + const value = window.localStorage.getItem(STORAGE_KEY) + return value === 'light' || value === 'dark' ? value : null + } catch { + return null + } +} + +function getSystemTheme(): Theme { + return window.matchMedia(MEDIA_QUERY).matches ? 'dark' : 'light' +} + +export function useDarkMode() { + const [theme, setThemeState] = useState(() => { + if (typeof window === 'undefined') { + return 'light' + } + + return getStoredTheme() ?? getSystemTheme() + }) + + useEffect(() => { + document.documentElement.setAttribute('data-theme', theme) + + try { + window.localStorage.setItem(STORAGE_KEY, theme) + } catch { + // ignore storage errors + } + }, [theme]) + + useEffect(() => { + if (typeof window === 'undefined') { + return + } + + const mediaQuery = window.matchMedia(MEDIA_QUERY) + + const handleChange = (event: MediaQueryListEvent) => { + const storedTheme = getStoredTheme() + + if (!storedTheme) { + setThemeState(event.matches ? 'dark' : 'light') + } + } + + if (typeof mediaQuery.addEventListener === 'function') { + mediaQuery.addEventListener('change', handleChange) + return () => mediaQuery.removeEventListener('change', handleChange) + } + + mediaQuery.addListener(handleChange) + return () => mediaQuery.removeListener(handleChange) + }, []) + + const setTheme = (nextTheme: Theme) => { + setThemeState(nextTheme) + } + + const toggleTheme = () => { + setThemeState(prev => (prev === 'light' ? 'dark' : 'light')) + } + + return { + theme, + isDark: theme === 'dark', + setTheme, + toggleTheme + } +}