use-theme.tsx 2.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. 'use client';
  2. import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
  3. type Theme = 'light' | 'dark' | 'system';
  4. interface ThemeContextType {
  5. theme: Theme;
  6. setTheme: (theme: Theme) => void;
  7. resolvedTheme: 'light' | 'dark';
  8. }
  9. const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
  10. export function ThemeProvider({ children }: { children: ReactNode }) {
  11. const [theme, setThemeState] = useState<Theme>('system');
  12. const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>('light');
  13. const resolvedTheme = theme === 'system' ? systemTheme : theme;
  14. // Hydrate from localStorage after mount (avoids SSR mismatch)
  15. /* eslint-disable react-hooks/set-state-in-effect -- Hydration from localStorage must happen in effect */
  16. useEffect(() => {
  17. const stored = localStorage.getItem('theme') as Theme | null;
  18. if (stored && ['light', 'dark', 'system'].includes(stored)) {
  19. setThemeState(stored);
  20. }
  21. setSystemTheme(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
  22. }, []);
  23. /* eslint-enable react-hooks/set-state-in-effect */
  24. // Apply theme to document
  25. useEffect(() => {
  26. const root = document.documentElement;
  27. if (resolvedTheme === 'dark') {
  28. root.classList.add('dark');
  29. } else {
  30. root.classList.remove('dark');
  31. }
  32. }, [resolvedTheme]);
  33. // Listen to system theme changes
  34. useEffect(() => {
  35. const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
  36. const handleChange = () => {
  37. setSystemTheme(mediaQuery.matches ? 'dark' : 'light');
  38. };
  39. mediaQuery.addEventListener('change', handleChange);
  40. return () => mediaQuery.removeEventListener('change', handleChange);
  41. }, []);
  42. // Save theme to localStorage
  43. const handleSetTheme = (newTheme: Theme) => {
  44. setThemeState(newTheme);
  45. localStorage.setItem('theme', newTheme);
  46. };
  47. return (
  48. <ThemeContext.Provider value={{ theme, setTheme: handleSetTheme, resolvedTheme }}>
  49. {children}
  50. </ThemeContext.Provider>
  51. );
  52. }
  53. export function useTheme() {
  54. const context = useContext(ThemeContext);
  55. if (!context) {
  56. throw new Error('useTheme must be used within ThemeProvider');
  57. }
  58. return context;
  59. }