shadcn-ui / ui

Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source.
https://ui.shadcn.com
MIT License
64.45k stars 3.66k forks source link

[bug]: useTheme (system) does not update based on user preferences #4094

Open joaopaulomoraes opened 3 weeks ago

joaopaulomoraes commented 3 weeks ago

Describe the bug

Once the theme is set to system, when changing the operating system settings it does not reflect the changes correctly, only after refreshing the page. [Vite]

https://github.com/shadcn-ui/ui/assets/16243531/35e1d9aa-576e-4555-9d3f-a20166552541

Affected component/components

theme-provider

How to reproduce

Codesandbox/StackBlitz link

https://stackblitz.com/~/github.com/shadcn/vite-template?file=src/main.tsx

Logs

No response

System Info

Platform: Darwin
Arch: x86_64
Version: Darwin Kernel Version 23.5.0: Wed May  1 20:09:52 PDT 2024; root:xnu-10063.121.3~5/RELEASE_X86_64
Node: v20.13.1
PNPM: 9.1.3

Before submitting

joaopaulomoraes commented 3 weeks ago

I found a solution by just adding a listener to the media event so that the theme would be updated again.

import * as storage from 'actions/storage';
import {
  createContext,
  useContext,
  useEffect,
  useState
} from 'react';

type Theme = 'dark' | 'light' | 'system';

type ThemeProviderProps = {
  children: React.ReactNode;
  defaultTheme ? : Theme;
  storageKey ? : string;
};

type ThemeProviderState = {
  theme: Theme;
  setTheme: (theme: Theme) => void;
};

const ThemeProviderContext = createContext < ThemeProviderState > ({
  theme: 'system',
  setTheme: () => null
});

export const ThemeProvider = ({
  children,
  storageKey = '@theme',
  defaultTheme = 'system',
  ...props
}: ThemeProviderProps) => {
  const [theme, setTheme] = useState < Theme > (
    () => storage.get < Theme > (storageKey) || defaultTheme
  );

  useEffect(() => {
    const root = document.documentElement;
    const systemTheme = matchMedia('(prefers-color-scheme: dark)');

    const updateTheme = () => {
      root.classList.remove('light', 'dark');

      if (theme === 'system') {
        root.classList.add(systemTheme.matches ? 'dark' : 'light');
        return;
      }

      root.classList.add(theme);
    };

    updateTheme();
    systemTheme.addEventListener('change', updateTheme);

    return () => {
      systemTheme.removeEventListener('change', updateTheme);
    };
  }, [theme]);

  const value = {
    theme,
    setTheme: (newTheme: Theme) => {
      storage.set(storageKey, newTheme);
      setTheme(newTheme);
    }
  };

  return ( <
    ThemeProviderContext.Provider {
      ...props
    }
    value = {
      value
    } > {
      children
    } <
    /ThemeProviderContext.Provider>
  );
};

export const useTheme = () => {
  const context = useContext(ThemeProviderContext);

  if (context === null) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }

  return context;
};