Feb 5, 2023

lpubsppop01's site

I made color scheme changing to work in all pages in this site. I confused because that there seems to be various implementation methods for sharing context. Finally worked code is following (fixed on Feb 13, 2023).

// components/app-context.tsx

import { createContext, Dispatch, SetStateAction } from "react";

export type AppContextValue = {
  getColorScheme: () => string | null,
  setColorScheme: Dispatch<SetStateAction<string | null>>,
}
export const AppContext = createContext<AppContextValue>({
  getColorScheme: () => null,
  setColorScheme: (_) => { }
})
// components/top-bar.tsx
...
export default function TopBar() {
  ...
  const appContextValue = useContext(AppContext)

  const handleColorSchemeButtonClick: React.MouseEventHandler<HTMLButtonElement> = (_) => {
    if (appContextValue.getColorScheme() == 'dark') {
      document.documentElement.setAttribute('my-color-scheme', 'light')
      appContextValue.setColorScheme('light')
    } else {
      document.documentElement.setAttribute('my-color-scheme', 'dark')
      appContextValue.setColorScheme('dark')
    }
  };

  return (
    <>
      ...
            <button
              className={appContextValue.getColorScheme() == 'dark' ? styles.lightMode : styles.darkMode}
              onClick={handleColorSchemeButtonClick}>
              <Image
                src={
                  appContextValue.getColorScheme() == 'dark'
                    ? '/material_icons/light_mode_black_24dp.svg'
                    : '/material_icons/dark_mode_white_24dp.svg'
                }
                alt={appContextValue.getColorScheme() == 'dark' ? 'Light mode' : 'Dark mode'}
                width={d.ICON_SMALL} height={d.ICON_SMALL}
              />
            </button>
      ...
    </>
  )
// pages/_app.tsx
...
export default function App({ Component, pageProps }: AppProps) {
  const [colorScheme, setColorScheme] = useState<string | null>(null)
  const appContextValue: AppContextValue = {
    getColorScheme: () => colorScheme,
    setColorScheme: (value) => {
      setColorScheme(value)
      localStorage.setItem('colorScheme', value as string)
    }
  }
  useEffect(() => {
    let colorScheme = appContextValue.getColorScheme()
    if (colorScheme == null) {
      let loadedColorScheme = localStorage.getItem('colorScheme')
      let setsDark = loadedColorScheme == 'dark'
      if (loadedColorScheme == null) {
        setsDark = window.matchMedia('(prefers-color-scheme: dark)').matches
      }
      if (setsDark) {
        document.documentElement.setAttribute('my-color-scheme', 'dark')

        // Detect Safari
        const lowerUserAgent = window.navigator.userAgent.toLowerCase()
        const browserIsSafari =
          lowerUserAgent.indexOf('chrome') == -1 &&
          lowerUserAgent.indexOf('safari') != -1

        // Set color scheme to app context
        if (browserIsSafari) {
          // Set with a delay on Safari to ensure updates
          setTimeout(() => {
            appContextValue.setColorScheme('dark')
          }, 200)
        } else {
          // Set normally on other browsers
          appContextValue.setColorScheme('dark')
        }
      }
    }
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
      if (e.matches && appContextValue.getColorScheme() != 'dark') {
        document.documentElement.setAttribute('my-color-scheme', 'dark')
        appContextValue.setColorScheme('dark')
      } else {
        document.documentElement.setAttribute('my-color-scheme', 'light')
        appContextValue.setColorScheme('light')
      }
    })
  });

  ...

  return (
    <AppContext.Provider value={appContextValue}>
      <Component {...pageProps} />
    </AppContext.Provider>
  )
}

The color scheme getter was the property colorScheme at first, but misuse of setter took time. So I changed it to the function getColorScheme().

It may be not good for performance, but used the above this time.

References: