From 49d4c13ab3475e37c2e406b9682492bf99d399d1 Mon Sep 17 00:00:00 2001 From: Aris Ripandi Date: Thu, 19 Sep 2024 02:47:49 +0700 Subject: [PATCH] refactor: revamped theme provider --- app/components/theme.tsx | 28 ++--- app/context/providers/theme-provider.tsx | 125 +++++++++++------------ app/styles.css | 2 +- app/utils/theme.server.ts | 4 +- 4 files changed, 67 insertions(+), 92 deletions(-) diff --git a/app/components/theme.tsx b/app/components/theme.tsx index eb5b2c0..5679a9a 100644 --- a/app/components/theme.tsx +++ b/app/components/theme.tsx @@ -1,33 +1,17 @@ -import { useState } from 'react' -import { Theme, useTheme } from '#/context/providers/theme-provider' +import { Theme, type ThemeType, useTheme } from '#/context/providers/theme-provider' import { clx } from '#/utils/ui-helper' -const themes = [ - { id: Theme.LIGHT, name: 'Light' }, - { id: Theme.DARK, name: 'Dark' }, - { id: Theme.SYSTEM, name: 'System' }, -] as const - -type ThemeOption = (typeof themes)[number] - export default function ThemeSwitcher() { const [theme, setTheme] = useTheme() - const [selectedTheme, setSelectedTheme] = useState( - themes.find((t) => t.id === theme) || themes[0] - ) const handleThemeChange = (event: React.ChangeEvent) => { - const newTheme = themes.find((theme) => theme.id === (event.target.value as Theme)) - if (newTheme) { - setSelectedTheme(newTheme) - setTheme(newTheme.id) - } + setTheme(event.target.value as ThemeType) } return (
diff --git a/app/context/providers/theme-provider.tsx b/app/context/providers/theme-provider.tsx index 885ecaa..d0a3a14 100644 --- a/app/context/providers/theme-provider.tsx +++ b/app/context/providers/theme-provider.tsx @@ -1,116 +1,107 @@ -/*! - * Portions of this file are based on code from `mattstobbs/remix-dark-mode`. - * Credits to Matt Stobbs: https://github.com/mattstobbs/remix-dark-mode - */ - import { useFetcher, useRevalidator } from '@remix-run/react' -import { createContext, useContext, useEffect, useRef, useState } from 'react' +import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react' import type { Dispatch, SetStateAction } from 'react' import type { EnumValues } from '#/utils/common' -enum Theme { - DARK = 'dark', - LIGHT = 'light', - SYSTEM = 'system', -} +const Theme = { + DARK: 'dark', + LIGHT: 'light', + SYSTEM: 'system', +} as const + +type ThemeType = EnumValues -type ThemeContextType = [Theme | null, Dispatch>] +type ThemeContextType = [ThemeType | null, Dispatch>] const ThemeContext = createContext(undefined) -const themes: Theme[] = Object.values(Theme) const prefersLightMQ = '(prefers-color-scheme: light)' -const prefersDarkMQ = '(prefers-color-scheme: dark)' -const getPreferredTheme = (): Theme => { +const getTheme = (): ThemeType => { if (typeof window === 'undefined') return Theme.SYSTEM - if (window.matchMedia(prefersLightMQ).matches) return Theme.LIGHT - if (window.matchMedia(prefersDarkMQ).matches) return Theme.DARK - return Theme.SYSTEM -} - -const getThemeFromSystem = (): Theme.LIGHT | Theme.DARK => { - if (typeof window === 'undefined') return Theme.LIGHT return window.matchMedia(prefersLightMQ).matches ? Theme.LIGHT : Theme.DARK } interface ThemeProviderProps { children: React.ReactNode - specifiedTheme?: EnumValues + specifiedTheme?: ThemeType } -function ThemeProvider({ children, specifiedTheme }: ThemeProviderProps) { - const [theme, setTheme] = useState(() => { - if (specifiedTheme && themes.includes(specifiedTheme)) { - return specifiedTheme - } - return null - }) - +function useThemeEffect(theme: ThemeType | null) { const { revalidate } = useRevalidator() const persistTheme = useFetcher() - const mountRun = useRef(false) - // biome-ignore lint/correctness/useExhaustiveDependencies: need to render once + const setThemeClass = useCallback((newTheme: ThemeType) => { + document.documentElement.classList.remove(Theme.LIGHT, Theme.DARK) + document.documentElement.classList.add(newTheme) + }, []) + + // biome-ignore lint/correctness/useExhaustiveDependencies: render once useEffect(() => { - if (!mountRun.current) { - mountRun.current = true - setTheme(specifiedTheme || getPreferredTheme()) - return - } if (theme) { persistTheme.submit({ theme }, { action: 'set-theme', method: 'POST' }) - const resolvedTheme = theme === Theme.SYSTEM ? getThemeFromSystem() : theme - document.documentElement.classList.remove(Theme.LIGHT, Theme.DARK) - document.documentElement.classList.add(resolvedTheme) + const resolvedTheme = theme === Theme.SYSTEM ? getTheme() : theme + setThemeClass(resolvedTheme) } - }, [theme, specifiedTheme]) + }, [theme]) useEffect(() => { - if (typeof window === 'undefined') return const mediaQuery = window.matchMedia(prefersLightMQ) const handleChange = () => { if (theme === Theme.SYSTEM) { - const newTheme = getThemeFromSystem() - document.documentElement.classList.remove(Theme.LIGHT, Theme.DARK) - document.documentElement.classList.add(newTheme) + setThemeClass(getTheme()) } revalidate() } mediaQuery.addEventListener('change', handleChange) return () => mediaQuery.removeEventListener('change', handleChange) - }, [theme, revalidate]) + }, [theme, revalidate, setThemeClass]) +} + +function ThemeProvider({ children, specifiedTheme }: ThemeProviderProps) { + const [theme, setTheme] = useState(() => { + if (specifiedTheme && Object.values(Theme).includes(specifiedTheme)) { + return specifiedTheme + } + return null + }) + + const mountRun = useRef(false) + + useEffect(() => { + if (!mountRun.current) { + mountRun.current = true + setTheme(specifiedTheme ?? getTheme()) + } + }, [specifiedTheme]) + + useThemeEffect(theme) return {children} } function NonFlashOfWrongThemeEls({ ssrTheme }: { ssrTheme: boolean }) { const [theme] = useTheme() - - // Client-side theme resolution only - const resolvedTheme = - typeof window !== 'undefined' && theme === Theme.SYSTEM ? getThemeFromSystem() : theme - - // Handle color scheme for light/dark + const resolvedTheme = theme === Theme.SYSTEM ? getTheme() : theme const colorScheme = resolvedTheme === Theme.LIGHT ? 'light' : resolvedTheme === Theme.DARK ? 'dark' : 'light dark' + const setThemeScript = ` + (function() { + const theme = ${JSON.stringify(resolvedTheme)}; + const cl = document.documentElement.classList; + cl.remove('light', 'dark'); + cl.add(theme); + })(); + ` + return ( <> {!ssrTheme && (