11"use client" ;
22
3- import React , { createContext , useContext , useState , useEffect , useLayoutEffect } from "react" ;
3+ import React , { createContext , useContext , useState , useEffect , useLayoutEffect , useRef } from "react" ;
44
55export type Theme = "dark" | "light" ;
66
@@ -15,19 +15,22 @@ const ThemeContext = createContext<ThemeContextValue>({
1515} ) ;
1616
1717export function ThemeProvider ( { children } : { children : React . ReactNode } ) {
18- // Always start with "dark" on both server and client — matches the SSR default.
19- // useLayoutEffect runs before paint and syncs with whatever the inline theme-init
20- // script already applied to <html>, so there's no visible flash.
2118 const [ theme , setTheme ] = useState < Theme > ( "dark" ) ;
19+ // Track whether the initial DOM sync has completed — we must not write
20+ // localStorage until we've read the real theme from the DOM.
21+ const initialised = useRef ( false ) ;
2222
2323 useLayoutEffect ( ( ) => {
2424 const actual : Theme = document . documentElement . classList . contains ( "dark" ) ? "dark" : "light" ;
25- // eslint-disable-next-line react-hooks/set-state-in-effect
26- if ( actual !== "dark" ) setTheme ( actual ) ;
25+ initialised . current = true ;
26+ if ( actual !== theme ) setTheme ( actual ) ;
27+ // eslint-disable-next-line react-hooks/exhaustive-deps
2728 } , [ ] ) ;
2829
29- // Keep <html> class and localStorage in sync whenever theme changes
30+ // Keep <html> class and localStorage in sync — but skip the very first run
31+ // (when theme is still the SSR default and initialised is not yet true).
3032 useEffect ( ( ) => {
33+ if ( ! initialised . current ) return ;
3134 const root = document . documentElement ;
3235 if ( theme === "dark" ) {
3336 root . classList . add ( "dark" ) ;
0 commit comments