mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-30 10:48:05 +02:00
fix(theme): fix useColorMode().colorMode
leading to React hydration mismatches (#10954)
This commit is contained in:
parent
9d7ceec189
commit
396deedba4
2 changed files with 57 additions and 33 deletions
|
@ -25,7 +25,8 @@ type ContextValue = {
|
||||||
/** Set new color mode. */
|
/** Set new color mode. */
|
||||||
readonly setColorMode: (colorMode: ColorMode) => void;
|
readonly setColorMode: (colorMode: ColorMode) => void;
|
||||||
|
|
||||||
// TODO legacy APIs kept for retro-compatibility: deprecate them
|
// TODO Docusaurus v4
|
||||||
|
// legacy APIs kept for retro-compatibility: deprecate them
|
||||||
readonly isDarkTheme: boolean;
|
readonly isDarkTheme: boolean;
|
||||||
readonly setLightTheme: () => void;
|
readonly setLightTheme: () => void;
|
||||||
readonly setDarkTheme: () => void;
|
readonly setDarkTheme: () => void;
|
||||||
|
@ -47,22 +48,55 @@ export type ColorMode = (typeof ColorModes)[keyof typeof ColorModes];
|
||||||
const coerceToColorMode = (colorMode?: string | null): ColorMode =>
|
const coerceToColorMode = (colorMode?: string | null): ColorMode =>
|
||||||
colorMode === ColorModes.dark ? ColorModes.dark : ColorModes.light;
|
colorMode === ColorModes.dark ? ColorModes.dark : ColorModes.light;
|
||||||
|
|
||||||
const getInitialColorMode = (defaultMode: ColorMode | undefined): ColorMode =>
|
const ColorModeAttribute = {
|
||||||
ExecutionEnvironment.canUseDOM
|
get: () => {
|
||||||
? coerceToColorMode(document.documentElement.getAttribute('data-theme'))
|
return coerceToColorMode(
|
||||||
: coerceToColorMode(defaultMode);
|
document.documentElement.getAttribute('data-theme'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
set: (colorMode: ColorMode) => {
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
'data-theme',
|
||||||
|
coerceToColorMode(colorMode),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const readInitialColorMode = (): ColorMode => {
|
||||||
|
if (!ExecutionEnvironment.canUseDOM) {
|
||||||
|
throw new Error("Can't read initial color mode on the server");
|
||||||
|
}
|
||||||
|
return ColorModeAttribute.get();
|
||||||
|
};
|
||||||
|
|
||||||
const storeColorMode = (newColorMode: ColorMode) => {
|
const storeColorMode = (newColorMode: ColorMode) => {
|
||||||
ColorModeStorage.set(coerceToColorMode(newColorMode));
|
ColorModeStorage.set(coerceToColorMode(newColorMode));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The color mode state is initialized in useEffect on purpose
|
||||||
|
// to avoid a React hydration mismatch errors
|
||||||
|
// The useColorMode() hook value lags behind on purpose
|
||||||
|
// This helps users avoid hydration mismatch errors in their code
|
||||||
|
// See also https://github.com/facebook/docusaurus/issues/7986
|
||||||
|
function useColorModeState() {
|
||||||
|
const {
|
||||||
|
colorMode: {defaultMode},
|
||||||
|
} = useThemeConfig();
|
||||||
|
|
||||||
|
const [colorMode, setColorModeState] = useState(defaultMode);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setColorModeState(readInitialColorMode());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [colorMode, setColorModeState] as const;
|
||||||
|
}
|
||||||
|
|
||||||
function useContextValue(): ContextValue {
|
function useContextValue(): ContextValue {
|
||||||
const {
|
const {
|
||||||
colorMode: {defaultMode, disableSwitch, respectPrefersColorScheme},
|
colorMode: {defaultMode, disableSwitch, respectPrefersColorScheme},
|
||||||
} = useThemeConfig();
|
} = useThemeConfig();
|
||||||
const [colorMode, setColorModeState] = useState(
|
const [colorMode, setColorModeState] = useColorModeState();
|
||||||
getInitialColorMode(defaultMode),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// A site is deployed without disableSwitch
|
// A site is deployed without disableSwitch
|
||||||
|
@ -77,49 +111,38 @@ function useContextValue(): ContextValue {
|
||||||
const setColorMode = useCallback(
|
const setColorMode = useCallback(
|
||||||
(newColorMode: ColorMode | null, options: {persist?: boolean} = {}) => {
|
(newColorMode: ColorMode | null, options: {persist?: boolean} = {}) => {
|
||||||
const {persist = true} = options;
|
const {persist = true} = options;
|
||||||
|
|
||||||
if (newColorMode) {
|
if (newColorMode) {
|
||||||
|
ColorModeAttribute.set(newColorMode);
|
||||||
setColorModeState(newColorMode);
|
setColorModeState(newColorMode);
|
||||||
if (persist) {
|
if (persist) {
|
||||||
storeColorMode(newColorMode);
|
storeColorMode(newColorMode);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (respectPrefersColorScheme) {
|
if (respectPrefersColorScheme) {
|
||||||
setColorModeState(
|
const osColorMode = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
.matches
|
||||||
? ColorModes.dark
|
? ColorModes.dark
|
||||||
: ColorModes.light,
|
: ColorModes.light;
|
||||||
);
|
ColorModeAttribute.set(osColorMode);
|
||||||
|
setColorModeState(osColorMode);
|
||||||
} else {
|
} else {
|
||||||
|
ColorModeAttribute.set(defaultMode);
|
||||||
setColorModeState(defaultMode);
|
setColorModeState(defaultMode);
|
||||||
}
|
}
|
||||||
ColorModeStorage.del();
|
ColorModeStorage.del();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[respectPrefersColorScheme, defaultMode],
|
[setColorModeState, respectPrefersColorScheme, defaultMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.documentElement.setAttribute(
|
|
||||||
'data-theme',
|
|
||||||
coerceToColorMode(colorMode),
|
|
||||||
);
|
|
||||||
}, [colorMode]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (disableSwitch) {
|
if (disableSwitch) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const onChange = (e: StorageEvent) => {
|
return ColorModeStorage.listen((e) => {
|
||||||
if (e.key !== ColorModeStorageKey) {
|
setColorMode(coerceToColorMode(e.newValue));
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
const storedColorMode = ColorModeStorage.get();
|
|
||||||
if (storedColorMode !== null) {
|
|
||||||
setColorMode(coerceToColorMode(storedColorMode));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('storage', onChange);
|
|
||||||
return () => window.removeEventListener('storage', onChange);
|
|
||||||
}, [disableSwitch, setColorMode]);
|
}, [disableSwitch, setColorMode]);
|
||||||
|
|
||||||
// PCS is coerced to light mode when printing, which causes the color mode to
|
// PCS is coerced to light mode when printing, which causes the color mode to
|
||||||
|
|
|
@ -9,6 +9,7 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
import type {PrismTheme} from 'prism-react-renderer';
|
import type {PrismTheme} from 'prism-react-renderer';
|
||||||
import type {DeepPartial} from 'utility-types';
|
import type {DeepPartial} from 'utility-types';
|
||||||
import type {MagicCommentConfig} from './codeBlockUtils';
|
import type {MagicCommentConfig} from './codeBlockUtils';
|
||||||
|
import type {ColorMode} from '../contexts/colorMode';
|
||||||
|
|
||||||
export type DocsVersionPersistence = 'localStorage' | 'none';
|
export type DocsVersionPersistence = 'localStorage' | 'none';
|
||||||
|
|
||||||
|
@ -44,7 +45,7 @@ export type Navbar = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ColorModeConfig = {
|
export type ColorModeConfig = {
|
||||||
defaultMode: 'light' | 'dark';
|
defaultMode: ColorMode;
|
||||||
disableSwitch: boolean;
|
disableSwitch: boolean;
|
||||||
respectPrefersColorScheme: boolean;
|
respectPrefersColorScheme: boolean;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue