Skip to content

Commit d9bee25

Browse files
committed
♻️ Refactor ThemeProvider
1 parent 70f2224 commit d9bee25

1 file changed

Lines changed: 112 additions & 8 deletions

File tree

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,115 @@
1-
"use client"
1+
import {
2+
createContext,
3+
useCallback,
4+
useContext,
5+
useEffect,
6+
useState,
7+
} from "react"
28

3-
import * as React from "react"
4-
import { ThemeProvider as NextThemesProvider } from "next-themes"
9+
export type Theme = "dark" | "light" | "system"
10+
11+
type ThemeProviderProps = {
12+
children: React.ReactNode
13+
defaultTheme?: Theme
14+
storageKey?: string
15+
}
16+
17+
type ThemeProviderState = {
18+
theme: Theme
19+
resolvedTheme: "dark" | "light"
20+
setTheme: (theme: Theme) => void
21+
}
22+
23+
const initialState: ThemeProviderState = {
24+
theme: "system",
25+
resolvedTheme: "light",
26+
setTheme: () => null,
27+
}
28+
29+
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
530

631
export function ThemeProvider({
7-
children,
8-
...props
9-
}: React.ComponentProps<typeof NextThemesProvider>) {
10-
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
11-
}
32+
children,
33+
defaultTheme = "system",
34+
storageKey = "vite-ui-theme",
35+
...props
36+
}: ThemeProviderProps) {
37+
const [theme, setTheme] = useState<Theme>(
38+
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
39+
)
40+
41+
const getResolvedTheme = useCallback((theme: Theme): "dark" | "light" => {
42+
if (theme === "system") {
43+
return window.matchMedia("(prefers-color-scheme: dark)").matches
44+
? "dark"
45+
: "light"
46+
}
47+
return theme
48+
}, [])
49+
50+
const [resolvedTheme, setResolvedTheme] = useState<"dark" | "light">(() =>
51+
getResolvedTheme(theme),
52+
)
53+
54+
const updateTheme = useCallback((newTheme: Theme) => {
55+
const root = window.document.documentElement
56+
57+
root.classList.remove("light", "dark")
58+
59+
if (newTheme === "system") {
60+
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
61+
.matches
62+
? "dark"
63+
: "light"
64+
65+
root.classList.add(systemTheme)
66+
return
67+
}
68+
69+
root.classList.add(newTheme)
70+
}, [])
71+
72+
useEffect(() => {
73+
updateTheme(theme)
74+
setResolvedTheme(getResolvedTheme(theme))
75+
76+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
77+
78+
const handleChange = () => {
79+
if (theme === "system") {
80+
updateTheme("system")
81+
setResolvedTheme(getResolvedTheme("system"))
82+
}
83+
}
84+
85+
mediaQuery.addEventListener("change", handleChange)
86+
87+
return () => {
88+
mediaQuery.removeEventListener("change", handleChange)
89+
}
90+
}, [theme, updateTheme, getResolvedTheme])
91+
92+
const value = {
93+
theme,
94+
resolvedTheme,
95+
setTheme: (theme: Theme) => {
96+
localStorage.setItem(storageKey, theme)
97+
setTheme(theme)
98+
},
99+
}
100+
101+
return (
102+
<ThemeProviderContext.Provider {...props} value={value}>
103+
{children}
104+
</ThemeProviderContext.Provider>
105+
)
106+
}
107+
108+
export const useTheme = () => {
109+
const context = useContext(ThemeProviderContext)
110+
111+
if (context === undefined)
112+
throw new Error("useTheme must be used within a ThemeProvider")
113+
114+
return context
115+
}

0 commit comments

Comments
 (0)