diff --git a/.changeset/themeprovider-ssr-auto.md b/.changeset/themeprovider-ssr-auto.md new file mode 100644 index 00000000000..a3c2e2eb7ea --- /dev/null +++ b/.changeset/themeprovider-ssr-auto.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Fixes a bug for theming with server side rendering where the output of the server and client mismatch [#1773](https://github.com/primer/react/issues/1773) diff --git a/docs/content/theming.md b/docs/content/theming.md index 7fc0840b3bc..79faf1cfe8e 100644 --- a/docs/content/theming.md +++ b/docs/content/theming.md @@ -176,6 +176,16 @@ function Example() { } ``` +#### `preventSSRMismatch` prop + +If you are doing server-side rendering, pass the `preventSSRMismatch` prop to ensure the rendered output from the server and browser match even when they resolve "auto" color mode differently. + +```jsx + + ... + +``` + ### Setting color schemes To choose which color schemes will be displayed in `day` and `night` mode, use the `dayScheme` and `nightScheme` props on `ThemeProvider` or the `setDayScheme` and `setNightScheme` functions from the `useTheme` hook: diff --git a/src/ThemeProvider.tsx b/src/ThemeProvider.tsx index 1c314ff4052..b1bd76846e7 100644 --- a/src/ThemeProvider.tsx +++ b/src/ThemeProvider.tsx @@ -17,6 +17,7 @@ export type ThemeProviderProps = { colorMode?: ColorModeWithAuto dayScheme?: string nightScheme?: string + preventSSRMismatch?: boolean } const ThemeContext = React.createContext<{ @@ -47,21 +48,50 @@ export const ThemeProvider: React.FC = ({children, ...props} // Initialize state const theme = props.theme ?? fallbackTheme ?? defaultTheme + + const resolvedColorModePassthrough = React.useRef( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This custom variable does not exist on window because we set it outselves + typeof window !== 'undefined' ? window.__PRIMER_RESOLVED_SERVER_COLOR_MODE : undefined + ) + const [colorMode, setColorMode] = React.useState(props.colorMode ?? fallbackColorMode ?? defaultColorMode) const [dayScheme, setDayScheme] = React.useState(props.dayScheme ?? fallbackDayScheme ?? defaultDayScheme) const [nightScheme, setNightScheme] = React.useState(props.nightScheme ?? fallbackNightScheme ?? defaultNightScheme) const systemColorMode = useSystemColorMode() - const resolvedColorMode = resolveColorMode(colorMode, systemColorMode) + const resolvedColorMode = resolvedColorModePassthrough.current || resolveColorMode(colorMode, systemColorMode) const colorScheme = chooseColorScheme(resolvedColorMode, dayScheme, nightScheme) const {resolvedTheme, resolvedColorScheme} = React.useMemo( () => applyColorScheme(theme, colorScheme), [theme, colorScheme] ) + // this effect will only run on client + React.useEffect( + function updateColorModeAfterServerPassthorugh() { + const resolvedColorModeOnClient = resolveColorMode(colorMode, systemColorMode) + + if (resolvedColorModePassthrough.current) { + // if the resolved color mode passed on from the server is not the resolved color mode on client, change it! + if (resolvedColorModePassthrough.current !== resolvedColorModeOnClient) { + window.setTimeout(() => { + // override colorMode to whatever is resolved on the client to get a re-render + setColorMode(resolvedColorModeOnClient) + // immediately after that, set the colorMode to what the user passed to respond to system color mode changes + setColorMode(colorMode) + }) + } + + resolvedColorModePassthrough.current = null + } + }, + [colorMode, systemColorMode] + ) + // Update state if props change React.useEffect(() => { setColorMode(props.colorMode ?? fallbackColorMode ?? defaultColorMode) - }, [props.colorMode, fallbackColorMode]) + }, [props.colorMode, resolvedColorMode, fallbackColorMode]) React.useEffect(() => { setDayScheme(props.dayScheme ?? fallbackDayScheme ?? defaultDayScheme) @@ -86,7 +116,12 @@ export const ThemeProvider: React.FC = ({children, ...props} setNightScheme }} > - {children} + + {children} + {props.preventSSRMismatch ? ( +