docusaurus
docusaurus copied to clipboard
Values derived from useColorMode() can be stale when rendering in prod mode
Have you read the Contributing Guidelines on issues?
- [X] I have read the Contributing Guidelines on issues.
Prerequisites
- [X] I'm using the latest version of Docusaurus.
- [X] I have tried the
npm run clear
oryarn clear
command. - [X] I have tried
rm -rf node_modules yarn.lock package-lock.json
and re-installing packages. - [X] I have tried creating a repro with https://new.docusaurus.io.
- [X] I have read the console error message carefully (if applicable).
Description
The value returned by the useColorMode
hook from @docusaurus/theme-common
seems to have a strange behavior when:
- Building for production and/or using React in prod mode (
npm run build && npm run serve
). -
window.localStorage.theme
is'dark'
while thethemeConfig.colorMode.defaultMode
value is'light'
, orwindow.localStorage.theme
is'light'
while thethemeConfig.colorMode.defaultMode
value is'dark'
.
Then trying to use the colorMode
value set the value of DOM attributes ends up generating a DOM with incorrect attribute values:
import { useColorMode } from "@docusaurus/theme-common";
import React from "react";
export default function ColorModeTest() {
const { colorMode } = useColorMode();
return (
<div title={colorMode}>{colorMode}</div>
);
}
In the conditions described above, if the themeConfig.colorMode.defaultMode
value is 'light'
and the localStorage.theme
value is 'dark'
:
- The generated HTML will be:
<div title="light">light</div>
- The hydrated DOM will be:
<div title="light">dark</div>
This is particularly troublesome when you have UI components that support both dark and light themes and rely on HTML attributes to set the correct color theme on the component itself (e.g. in order to display a light component in a dark context for visual emphasis).
Reproducible demo
https://github.com/fvsch/docusaurus-use-color-mode-stale-value
Steps to reproduce
- Check out https://github.com/fvsch/docusaurus-use-color-mode-stale-value
- Install dependencies, build for prod and serve that build:
npm install && npm run build && npm run serve
- The home page of the demo should be using a light theme.
- Click the theme switching button on the top right to switch to the dark theme.
- Reload the page.
Expected behavior
The values derived from the colorMode
, whether they're text nodes or attribute nodes, should be in sync and reflect the localStorage.theme
value.
In the last step, the visual result should be:
Actual behavior
Attribute values that are derived from the colorMode
value seem to be outdated.
In the last step, the visual result is:
Your environment
- Public source code: https://github.com/fvsch/docusaurus-use-color-mode-stale-value/blob/main/src/components/ColorModeTest/index.js
- Public site URL: n.a. (but currently affects https://developer.stackblitz.com/ too)
- Docusaurus version used: 2.0.1
- React version used: 17.0.2
- Environment name and version (e.g. Chrome 89, Node.js 16.4): all browsers tested (Firefox, Chrome and Safari); Node 16.17.0.
- Operating system and version (e.g. Ubuntu 20.04.2 LTS): macOS 12.5, probably happens on Linux too (happens when building our site on CI); haven't tried on Windows but I don't expect a difference here.
Self-service
- [ ] I'd be willing to fix this bug myself.
Quick StackBlitz repro link: https://stackblitz.com/github/fvsch/docusaurus-use-color-mode-stale-value?file=src%2Fcomponents%2FColorModeTest%2Findex.js
Agree there is likely a React hydration problem.
const getInitialColorMode = (defaultMode: ColorMode | undefined): ColorMode =>
ExecutionEnvironment.canUseDOM
? coerceToColorMode(document.documentElement.getAttribute('data-theme'))
: coerceToColorMode(defaultMode);
const [colorMode, setColorModeState] = useState(
getInitialColorMode(defaultMode),
);
Again related blog post: https://www.joshwcomeau.com/react/the-perils-of-rehydration/
Afaik it has been claimed in a clearer way more recently by the React core team that the SSR/CSR must 100% match. React 18 introduces a onRecoverableError
callback which usually notice you of hydration mismatches, and in this React 18 POC PR (https://github.com/facebook/docusaurus/pull/7855) I noticed we had some.
In this case I think we can't technically init our React state with the correct value, but instead should always init it to the default color mode, and then trigger a new re-render to fix it after hydration.
const [colorMode, setColorModeState] = useState(defaultMode);
useLayoutEffect(() => {
coerceToColorMode(document.documentElement.getAttribute('data-theme'))
},[])
This should fix the SSR/CSR mismatch, but this is a bit annoying unfortunately, as it will mean some components will render first with the wrong colorMode and then eventually re-render with the right one
any status update or timeline on this? seems to be an important one. I have an logo with a font color that I need to change dynamically on the current color theme, and currently this bug means I need to keep the theme switcher toggle disabled for now. Thanks!
I'll probably fix this as part of the React 18 upgrade, as we'll be able to see hydration error messages that I would have to fix one by one to complete this migration.
Note you might not need useColorMode
, you can also use html[data-theme="dark"
CSS selectors and we also have a ThemedImage
component: https://docusaurus.io/docs/markdown-features/assets#themed-images
Is there any ETA on that? Still broken on 3.0.1
@sszczep this is likely not something we can actually fix. Due to how React hydration works, the first value returned by this hook is likely not the actual effective color mode your page is rendered in.
I suggested in https://github.com/facebook/docusaurus/issues/7986#issuecomment-1291989001 to use CSS media queries for styling instead.
If you have another use-case that can't be solved with CSS, please explain why and I'll try to provide a workaround.
@sszczep this is likely not something we can actually fix. Due to how React hydration works, the first value returned by this hook is likely not the actual effective color mode your page is rendered in.
I suggested in https://github.com/facebook/docusaurus/issues/7986#issuecomment-1291989001 to use CSS media queries for styling instead.
If you have another use-case that can't be solved with CSS, please explain why and I'll try to provide a workaround.
I need to pass current color mode to Material UI Theme Provider. ~~Can't use CSS variables unfortunately~~. If you have a workaround that would be great. Cheers
Edit: Turns out that you can use MUI with CSS theme variables. Please check my other reply in that thread: https://github.com/facebook/docusaurus/issues/7986#issuecomment-1921756457
There is no good workaround IMHO.
You might be fine with the following, but use at your own risks, and be aware it is likely to create issues down the line.
const isDarkMode = document.documentElement.getAttribute('data-theme') === "dark";
Please also understand that Docusaurus doesn't have good support for "legacy CSS-in-JS libs" (StyledComponents, Emotion, JSS, MUI...), that require collecting styles during the rendering process (see also https://github.com/facebook/docusaurus/issues/3236).
So I'd simply recommend not using MUI in the first place, unless you clearly understand the tradeoff you make and the risk of having flashes of unstyled content.
Turns out that with MUI v5.6.0 they released a support for CSS theme variables. I successfully incorporated CssVarsProvider with Docusaurus theming system. They are now synced without any further gimmicks and shouldn't cause issues in the future. As described here it has some drawbacks (bigger code size, polluting stylesheets with vars) but it is definitely better than dealing with FOUC.
@slorber I also tested your recommendation, worked fine with MutationObserver, but the flicker was worse than I initially anticipated.
If anyone is interested, here is the code:
src/theme/Layout/MuiThemeProvider.tsx
import React from "react";
import {
Experimental_CssVarsProvider as CssVarsProvider,
experimental_extendTheme as extendTheme,
} from "@mui/material/styles";
const theme = extendTheme({
// custom theming overrides...
});
export default function MuiThemeProvider({
children,
}: React.PropsWithChildren) {
return (
<CssVarsProvider
theme={theme}
attribute="data-theme"
colorSchemeStorageKey="theme"
modeStorageKey="theme"
>
{children}
</CssVarsProvider>
);
}
src/theme/Layout/index.tsx
import React from "react";
import Layout from "@theme-original/Layout";
import { Props } from "@theme/Layout";
import MuiThemeProvider from "./MuiThemeProvider";
export default function LayoutWrapper({ children, ...props }: Props) {
return (
<Layout {...props}>
<MuiThemeProvider>
{children}
</MuiThemeProvider>
</Layout>
);
}
Once again, thanks for guiding me into that direction.
I'll probably fix this as part of the React 18 upgrade, as we'll be able to see hydration error messages that I would have to fix one by one to complete this migration.
Note you might not need
useColorMode
, you can also usehtml[data-theme="dark"
CSS selectors and we also have aThemedImage
component: https://docusaurus.io/docs/markdown-features/assets#themed-images
I had the same problem and "ThemedImage" helped
@LeonnardoVerol which problem, and can you create a https://docusaurus.new repro please so that we see this problem in action?
@LeonnardoVerol which problem, and can you create a https://docusaurus.new repro please so that we see this problem in action?
It was like the original post... I was using "colorMode" to swap 2 images. Docusaurus would swap correctly the images on light/dark toggle but fail to swap the images in some other situations. (E.g: on page refresh) Replacing that logic for "ThemedImage" solved my problem.