@@ -25,7 +25,8 @@ type ContextValue = {
25
25
/** Set new color mode. */
26
26
readonly setColorMode : ( colorMode : ColorMode ) => void ;
27
27
28
- // TODO legacy APIs kept for retro-compatibility: deprecate them
28
+ // TODO Docusaurus v4
29
+ // legacy APIs kept for retro-compatibility: deprecate them
29
30
readonly isDarkTheme : boolean ;
30
31
readonly setLightTheme : ( ) => void ;
31
32
readonly setDarkTheme : ( ) => void ;
@@ -47,22 +48,55 @@ export type ColorMode = (typeof ColorModes)[keyof typeof ColorModes];
47
48
const coerceToColorMode = ( colorMode ?: string | null ) : ColorMode =>
48
49
colorMode === ColorModes . dark ? ColorModes . dark : ColorModes . light ;
49
50
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
+ } ;
54
71
55
72
const storeColorMode = ( newColorMode : ColorMode ) => {
56
73
ColorModeStorage . set ( coerceToColorMode ( newColorMode ) ) ;
57
74
} ;
58
75
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
+
59
95
function useContextValue ( ) : ContextValue {
60
96
const {
61
97
colorMode : { defaultMode, disableSwitch, respectPrefersColorScheme} ,
62
98
} = useThemeConfig ( ) ;
63
- const [ colorMode , setColorModeState ] = useState (
64
- getInitialColorMode ( defaultMode ) ,
65
- ) ;
99
+ const [ colorMode , setColorModeState ] = useColorModeState ( ) ;
66
100
67
101
useEffect ( ( ) => {
68
102
// A site is deployed without disableSwitch
@@ -77,49 +111,38 @@ function useContextValue(): ContextValue {
77
111
const setColorMode = useCallback (
78
112
( newColorMode : ColorMode | null , options : { persist ?: boolean } = { } ) => {
79
113
const { persist = true } = options ;
114
+
80
115
if ( newColorMode ) {
116
+ ColorModeAttribute . set ( newColorMode ) ;
81
117
setColorModeState ( newColorMode ) ;
82
118
if ( persist ) {
83
119
storeColorMode ( newColorMode ) ;
84
120
}
85
121
} else {
86
122
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 ) ;
92
129
} else {
130
+ ColorModeAttribute . set ( defaultMode ) ;
93
131
setColorModeState ( defaultMode ) ;
94
132
}
95
133
ColorModeStorage . del ( ) ;
96
134
}
97
135
} ,
98
- [ respectPrefersColorScheme , defaultMode ] ,
136
+ [ setColorModeState , respectPrefersColorScheme , defaultMode ] ,
99
137
) ;
100
138
101
- useEffect ( ( ) => {
102
- document . documentElement . setAttribute (
103
- 'data-theme' ,
104
- coerceToColorMode ( colorMode ) ,
105
- ) ;
106
- } , [ colorMode ] ) ;
107
-
108
139
useEffect ( ( ) => {
109
140
if ( disableSwitch ) {
110
141
return undefined ;
111
142
}
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
+ } ) ;
123
146
} , [ disableSwitch , setColorMode ] ) ;
124
147
125
148
// PCS is coerced to light mode when printing, which causes the color mode to
0 commit comments