@@ -25,7 +25,8 @@ type ContextValue = {
2525 /** Set new color mode. */
2626 readonly setColorMode : ( colorMode : ColorMode ) => void ;
2727
28- // TODO legacy APIs kept for retro-compatibility: deprecate them
28+ // TODO Docusaurus v4
29+ // legacy APIs kept for retro-compatibility: deprecate them
2930 readonly isDarkTheme : boolean ;
3031 readonly setLightTheme : ( ) => void ;
3132 readonly setDarkTheme : ( ) => void ;
@@ -47,22 +48,55 @@ export type ColorMode = (typeof ColorModes)[keyof typeof ColorModes];
4748const coerceToColorMode = ( colorMode ?: string | null ) : ColorMode =>
4849 colorMode === ColorModes . dark ? ColorModes . dark : ColorModes . light ;
4950
50- const getInitialColorMode = ( defaultMode : ColorMode | undefined ) : ColorMode =>
51- ExecutionEnvironment . canUseDOM
52- ? coerceToColorMode ( document . documentElement . getAttribute ( 'data-theme' ) )
53- : coerceToColorMode ( defaultMode ) ;
51+ const ColorModeAttribute = {
52+ get : ( ) => {
53+ return coerceToColorMode (
54+ document . documentElement . getAttribute ( 'data-theme' ) ,
55+ ) ;
56+ } ,
57+ set : ( colorMode : ColorMode ) => {
58+ document . documentElement . setAttribute (
59+ 'data-theme' ,
60+ coerceToColorMode ( colorMode ) ,
61+ ) ;
62+ } ,
63+ } ;
64+
65+ const readInitialColorMode = ( ) : ColorMode => {
66+ if ( ! ExecutionEnvironment . canUseDOM ) {
67+ throw new Error ( "Can't read initial color mode on the server" ) ;
68+ }
69+ return ColorModeAttribute . get ( ) ;
70+ } ;
5471
5572const storeColorMode = ( newColorMode : ColorMode ) => {
5673 ColorModeStorage . set ( coerceToColorMode ( newColorMode ) ) ;
5774} ;
5875
76+ // The color mode state is initialized in useEffect on purpose
77+ // to avoid a React hydration mismatch errors
78+ // The useColorMode() hook value lags behind on purpose
79+ // This helps users avoid hydration mismatch errors in their code
80+ // See also https://github.com/facebook/docusaurus/issues/7986
81+ function useColorModeState ( ) {
82+ const {
83+ colorMode : { defaultMode} ,
84+ } = useThemeConfig ( ) ;
85+
86+ const [ colorMode , setColorModeState ] = useState ( defaultMode ) ;
87+
88+ useEffect ( ( ) => {
89+ setColorModeState ( readInitialColorMode ( ) ) ;
90+ } , [ ] ) ;
91+
92+ return [ colorMode , setColorModeState ] as const ;
93+ }
94+
5995function useContextValue ( ) : ContextValue {
6096 const {
6197 colorMode : { defaultMode, disableSwitch, respectPrefersColorScheme} ,
6298 } = useThemeConfig ( ) ;
63- const [ colorMode , setColorModeState ] = useState (
64- getInitialColorMode ( defaultMode ) ,
65- ) ;
99+ const [ colorMode , setColorModeState ] = useColorModeState ( ) ;
66100
67101 useEffect ( ( ) => {
68102 // A site is deployed without disableSwitch
@@ -77,49 +111,38 @@ function useContextValue(): ContextValue {
77111 const setColorMode = useCallback (
78112 ( newColorMode : ColorMode | null , options : { persist ?: boolean } = { } ) => {
79113 const { persist = true } = options ;
114+
80115 if ( newColorMode ) {
116+ ColorModeAttribute . set ( newColorMode ) ;
81117 setColorModeState ( newColorMode ) ;
82118 if ( persist ) {
83119 storeColorMode ( newColorMode ) ;
84120 }
85121 } else {
86122 if ( respectPrefersColorScheme ) {
87- setColorModeState (
88- window . matchMedia ( '(prefers-color-scheme: dark)' ) . matches
89- ? ColorModes . dark
90- : ColorModes . light ,
91- ) ;
123+ const osColorMode = window . matchMedia ( '(prefers-color-scheme: dark)' )
124+ . matches
125+ ? ColorModes . dark
126+ : ColorModes . light ;
127+ ColorModeAttribute . set ( osColorMode ) ;
128+ setColorModeState ( osColorMode ) ;
92129 } else {
130+ ColorModeAttribute . set ( defaultMode ) ;
93131 setColorModeState ( defaultMode ) ;
94132 }
95133 ColorModeStorage . del ( ) ;
96134 }
97135 } ,
98- [ respectPrefersColorScheme , defaultMode ] ,
136+ [ setColorModeState , respectPrefersColorScheme , defaultMode ] ,
99137 ) ;
100138
101- useEffect ( ( ) => {
102- document . documentElement . setAttribute (
103- 'data-theme' ,
104- coerceToColorMode ( colorMode ) ,
105- ) ;
106- } , [ colorMode ] ) ;
107-
108139 useEffect ( ( ) => {
109140 if ( disableSwitch ) {
110141 return undefined ;
111142 }
112- const onChange = ( e : StorageEvent ) => {
113- if ( e . key !== ColorModeStorageKey ) {
114- return ;
115- }
116- const storedColorMode = ColorModeStorage . get ( ) ;
117- if ( storedColorMode !== null ) {
118- setColorMode ( coerceToColorMode ( storedColorMode ) ) ;
119- }
120- } ;
121- window . addEventListener ( 'storage' , onChange ) ;
122- return ( ) => window . removeEventListener ( 'storage' , onChange ) ;
143+ return ColorModeStorage . listen ( ( e ) => {
144+ setColorMode ( coerceToColorMode ( e . newValue ) ) ;
145+ } ) ;
123146 } , [ disableSwitch , setColorMode ] ) ;
124147
125148 // PCS is coerced to light mode when printing, which causes the color mode to
0 commit comments