Skip to content

Commit a30b5c3

Browse files
committed
fix color mode hydration mismatch
1 parent 9d7ceec commit a30b5c3

File tree

2 files changed

+57
-33
lines changed

2 files changed

+57
-33
lines changed

packages/docusaurus-theme-common/src/contexts/colorMode.tsx

Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -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];
4748
const 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

5572
const 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+
5995
function 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

packages/docusaurus-theme-common/src/utils/useThemeConfig.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
99
import type {PrismTheme} from 'prism-react-renderer';
1010
import type {DeepPartial} from 'utility-types';
1111
import type {MagicCommentConfig} from './codeBlockUtils';
12+
import type {ColorMode} from '../contexts/colorMode';
1213

1314
export type DocsVersionPersistence = 'localStorage' | 'none';
1415

@@ -44,7 +45,7 @@ export type Navbar = {
4445
};
4546

4647
export type ColorModeConfig = {
47-
defaultMode: 'light' | 'dark';
48+
defaultMode: ColorMode;
4849
disableSwitch: boolean;
4950
respectPrefersColorScheme: boolean;
5051
};

0 commit comments

Comments
 (0)