From 4209ddd8ab7639e38b3ab04ae49ba644b5cdac7c Mon Sep 17 00:00:00 2001 From: Matthew Costabile Date: Tue, 24 Mar 2026 01:50:55 +0000 Subject: [PATCH 1/5] improve theme provider --- packages/react/src/ThemeProvider.tsx | 79 +++++++++---------- .../src/components/ThemeProvider.tsx | 79 +++++++++---------- 2 files changed, 76 insertions(+), 82 deletions(-) diff --git a/packages/react/src/ThemeProvider.tsx b/packages/react/src/ThemeProvider.tsx index 75a10b07b2b..3631a4395e2 100644 --- a/packages/react/src/ThemeProvider.tsx +++ b/packages/react/src/ThemeProvider.tsx @@ -1,5 +1,4 @@ import React from 'react' -import ReactDOM from 'react-dom' import defaultTheme from './theme' import deepmerge from 'deepmerge' import {useId} from './hooks' @@ -62,63 +61,61 @@ export const ThemeProvider: React.FC const theme = fallbackTheme ?? defaultTheme const uniqueDataId = useId() - const {resolvedServerColorMode} = getServerHandoff(uniqueDataId) - const resolvedColorModePassthrough = React.useRef(resolvedServerColorMode) + // Lazy initializer reads DOM + parses JSON once instead of every render + const [serverColorMode, setServerColorMode] = React.useState( + () => getServerHandoff(uniqueDataId).resolvedServerColorMode, + ) const [colorMode, setColorMode] = useSyncedState(props.colorMode ?? fallbackColorMode ?? defaultColorMode) const [dayScheme, setDayScheme] = useSyncedState(props.dayScheme ?? fallbackDayScheme ?? defaultDayScheme) const [nightScheme, setNightScheme] = useSyncedState(props.nightScheme ?? fallbackNightScheme ?? defaultNightScheme) const systemColorMode = useSystemColorMode() - // eslint-disable-next-line react-hooks/refs - const resolvedColorMode = resolvedColorModePassthrough.current || resolveColorMode(colorMode, systemColorMode) + const resolvedColorMode = serverColorMode ?? 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 + // After hydration, clear the server passthrough so client-side color mode takes over React.useEffect( - function updateColorModeAfterServerPassthrough() { - 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(() => { - // use ReactDOM.flushSync to prevent automatic batching of state updates since React 18 - // ref: https://github.com/reactwg/react-18/discussions/21 - ReactDOM.flushSync(() => { - // 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 + function clearServerPassthrough() { + if (serverColorMode !== undefined) { + setServerColorMode(undefined) } }, - [colorMode, systemColorMode, setColorMode], + [serverColorMode], + ) + + const contextValue = React.useMemo( + () => ({ + theme: resolvedTheme, + colorScheme, + colorMode, + resolvedColorMode, + resolvedColorScheme, + dayScheme, + nightScheme, + setColorMode, + setDayScheme, + setNightScheme, + }), + [ + resolvedTheme, + colorScheme, + colorMode, + resolvedColorMode, + resolvedColorScheme, + dayScheme, + nightScheme, + setColorMode, + setDayScheme, + setNightScheme, + ], ) return ( - +
const theme = props.theme ?? fallbackTheme ?? defaultTheme const uniqueDataId = useId() - const {resolvedServerColorMode} = getServerHandoff(uniqueDataId) - const resolvedColorModePassthrough = React.useRef(resolvedServerColorMode) + // Lazy initializer reads DOM + parses JSON once instead of every render + const [serverColorMode, setServerColorMode] = React.useState( + () => getServerHandoff(uniqueDataId).resolvedServerColorMode, + ) const [colorMode, setColorMode] = useSyncedState(props.colorMode ?? fallbackColorMode ?? defaultColorMode) const [dayScheme, setDayScheme] = useSyncedState(props.dayScheme ?? fallbackDayScheme ?? defaultDayScheme) const [nightScheme, setNightScheme] = useSyncedState(props.nightScheme ?? fallbackNightScheme ?? defaultNightScheme) const systemColorMode = useSystemColorMode() - // eslint-disable-next-line react-hooks/refs - const resolvedColorMode = resolvedColorModePassthrough.current || resolveColorMode(colorMode, systemColorMode) + const resolvedColorMode = serverColorMode ?? 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 + // After hydration, clear the server passthrough so client-side color mode takes over React.useEffect( - function updateColorModeAfterServerPassthrough() { - 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(() => { - // use ReactDOM.flushSync to prevent automatic batching of state updates since React 18 - // ref: https://github.com/reactwg/react-18/discussions/21 - ReactDOM.flushSync(() => { - // 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 + function clearServerPassthrough() { + if (serverColorMode !== undefined) { + setServerColorMode(undefined) } }, - [colorMode, systemColorMode, setColorMode], + [serverColorMode], + ) + + const contextValue = React.useMemo( + () => ({ + theme: resolvedTheme, + colorScheme, + colorMode, + resolvedColorMode, + resolvedColorScheme, + dayScheme, + nightScheme, + setColorMode, + setDayScheme, + setNightScheme, + }), + [ + resolvedTheme, + colorScheme, + colorMode, + resolvedColorMode, + resolvedColorScheme, + dayScheme, + nightScheme, + setColorMode, + setDayScheme, + setNightScheme, + ], ) return ( - + {children} {props.preventSSRMismatch ? ( From bc01f0d2658fa53cdc4f783ba6e1117101d0375b Mon Sep 17 00:00:00 2001 From: Matthew Costabile Date: Tue, 24 Mar 2026 01:59:52 +0000 Subject: [PATCH 2/5] guard setSystemColorMode to avoid redundant state update on mount --- packages/react/src/ThemeProvider.tsx | 7 ++++--- packages/styled-react/src/components/ThemeProvider.tsx | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/react/src/ThemeProvider.tsx b/packages/react/src/ThemeProvider.tsx index 3631a4395e2..746c8f772b5 100644 --- a/packages/react/src/ThemeProvider.tsx +++ b/packages/react/src/ThemeProvider.tsx @@ -161,9 +161,10 @@ function useSystemColorMode() { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (media) { - // just in case the preference changed before the event listener was attached - const isNight = media.matches - setSystemColorMode(matchesMediaToColorMode(isNight)) + // Only update if preference changed between useState init and effect + const currentMode = matchesMediaToColorMode(media.matches) + setSystemColorMode(prev => (prev === currentMode ? prev : currentMode)) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (media.addEventListener !== undefined) { media.addEventListener('change', handleChange) diff --git a/packages/styled-react/src/components/ThemeProvider.tsx b/packages/styled-react/src/components/ThemeProvider.tsx index cecec8075e6..8ef5bfa8ab4 100644 --- a/packages/styled-react/src/components/ThemeProvider.tsx +++ b/packages/styled-react/src/components/ThemeProvider.tsx @@ -157,9 +157,10 @@ function useSystemColorMode() { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (media) { - // just in case the preference changed before the event listener was attached - const isNight = media.matches - setSystemColorMode(matchesMediaToColorMode(isNight)) + // Only update if preference changed between useState init and effect + const currentMode = matchesMediaToColorMode(media.matches) + setSystemColorMode(prev => (prev === currentMode ? prev : currentMode)) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (media.addEventListener !== undefined) { media.addEventListener('change', handleChange) From 8cb69d13c85938f99a6daf52938fb30fc31a8ecb Mon Sep 17 00:00:00 2001 From: Matthew Costabile Date: Tue, 24 Mar 2026 02:00:38 +0000 Subject: [PATCH 3/5] add changeset --- .changeset/improve-theme-provider-perf.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/improve-theme-provider-perf.md diff --git a/.changeset/improve-theme-provider-perf.md b/.changeset/improve-theme-provider-perf.md new file mode 100644 index 00000000000..bcee0ddb628 --- /dev/null +++ b/.changeset/improve-theme-provider-perf.md @@ -0,0 +1,11 @@ +--- +"@primer/react": patch +"@primer/styled-react": patch +--- + +perf(ThemeProvider): Reduce unnecessary renders and effect cascades + +- Replace per-render DOM read + JSON.parse for SSR handoff with a lazy `useState` initializer (runs once) +- Replace complex SSR hydration effect (`setTimeout` → `flushSync` → two cascading `setColorMode` calls) with a single `setServerColorMode(undefined)` on mount +- Memoize context value object to prevent unnecessary re-renders of all consumers +- Guard `setSystemColorMode` in `useSystemColorMode` to avoid redundant state update on mount From 68d6fe4e614cbb8d3f25a6f67ea3554262d0822f Mon Sep 17 00:00:00 2001 From: Matthew Costabile Date: Wed, 25 Mar 2026 18:57:46 +0000 Subject: [PATCH 4/5] perf(ThemeProvider): use useSyncExternalStore for hydration and system color mode - Replace useState + useEffect SSR hydration handoff with useSyncExternalStore - Replace useState + useEffect in useSystemColorMode with useSyncExternalStore - Cache getServerHandoff DOM read + JSON.parse per ID - Remove post-hydration re-render and effect cascades --- .changeset/improve-theme-provider-perf.md | 6 +- packages/react/src/ThemeProvider.tsx | 95 +++++++------------ .../src/components/ThemeProvider.tsx | 95 +++++++------------ 3 files changed, 71 insertions(+), 125 deletions(-) diff --git a/.changeset/improve-theme-provider-perf.md b/.changeset/improve-theme-provider-perf.md index bcee0ddb628..93ae86578ad 100644 --- a/.changeset/improve-theme-provider-perf.md +++ b/.changeset/improve-theme-provider-perf.md @@ -5,7 +5,7 @@ perf(ThemeProvider): Reduce unnecessary renders and effect cascades -- Replace per-render DOM read + JSON.parse for SSR handoff with a lazy `useState` initializer (runs once) -- Replace complex SSR hydration effect (`setTimeout` → `flushSync` → two cascading `setColorMode` calls) with a single `setServerColorMode(undefined)` on mount +- Replace `useState` + `useEffect` SSR hydration handoff with `useSyncExternalStore` — eliminates post-hydration re-render +- Replace `useState` + `useEffect` in `useSystemColorMode` with `useSyncExternalStore` — eliminates effect gap and stale-then-update flicker +- Cache `getServerHandoff` DOM read + JSON.parse per ID (runs once, not on every call) - Memoize context value object to prevent unnecessary re-renders of all consumers -- Guard `setSystemColorMode` in `useSystemColorMode` to avoid redundant state update on mount diff --git a/packages/react/src/ThemeProvider.tsx b/packages/react/src/ThemeProvider.tsx index 746c8f772b5..a784626a1f6 100644 --- a/packages/react/src/ThemeProvider.tsx +++ b/packages/react/src/ThemeProvider.tsx @@ -38,16 +38,29 @@ const ThemeContext = React.createContext<{ }) // inspired from __NEXT_DATA__, we use application/json to avoid CSRF policy with inline scripts +const serverHandoffCache = new Map>() const getServerHandoff = (id: string) => { + const cached = serverHandoffCache.get(id) + if (cached !== undefined) return cached + try { const serverData = document.getElementById(`__PRIMER_DATA_${id}__`)?.textContent - if (serverData) return JSON.parse(serverData) + if (serverData) { + const parsed = JSON.parse(serverData) + serverHandoffCache.set(id, parsed) + return parsed + } } catch (_error) { // if document/element does not exist or JSON is invalid, suppress error } - return {} + + const empty = {} + serverHandoffCache.set(id, empty) + return empty } +const emptySubscribe = () => () => {} + export const ThemeProvider: React.FC> = ({children, ...props}) => { // Get fallback values from parent ThemeProvider (if exists) const { @@ -61,32 +74,25 @@ export const ThemeProvider: React.FC const theme = fallbackTheme ?? defaultTheme const uniqueDataId = useId() - // Lazy initializer reads DOM + parses JSON once instead of every render - const [serverColorMode, setServerColorMode] = React.useState( - () => getServerHandoff(uniqueDataId).resolvedServerColorMode, - ) const [colorMode, setColorMode] = useSyncedState(props.colorMode ?? fallbackColorMode ?? defaultColorMode) const [dayScheme, setDayScheme] = useSyncedState(props.dayScheme ?? fallbackDayScheme ?? defaultDayScheme) const [nightScheme, setNightScheme] = useSyncedState(props.nightScheme ?? fallbackNightScheme ?? defaultNightScheme) const systemColorMode = useSystemColorMode() - const resolvedColorMode = serverColorMode ?? resolveColorMode(colorMode, systemColorMode) + const clientColorMode = resolveColorMode(colorMode, systemColorMode) + // During SSR/hydration, use the server-rendered color mode from the handoff script tag + // to avoid mismatches. After hydration, resolve from client state. + const resolvedColorMode = React.useSyncExternalStore( + emptySubscribe, + () => clientColorMode, + () => getServerHandoff(uniqueDataId).resolvedServerColorMode ?? clientColorMode, + ) const colorScheme = chooseColorScheme(resolvedColorMode, dayScheme, nightScheme) const {resolvedTheme, resolvedColorScheme} = React.useMemo( () => applyColorScheme(theme, colorScheme), [theme, colorScheme], ) - // After hydration, clear the server passthrough so client-side color mode takes over - React.useEffect( - function clearServerPassthrough() { - if (serverColorMode !== undefined) { - setServerColorMode(undefined) - } - }, - [serverColorMode], - ) - const contextValue = React.useMemo( () => ({ theme: resolvedTheme, @@ -143,55 +149,22 @@ export function useColorSchemeVar(values: Partial>, fallb return values[colorScheme] ?? fallback } -function useSystemColorMode() { - const [systemColorMode, setSystemColorMode] = React.useState(getSystemColorMode) - - React.useEffect(() => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const media = window?.matchMedia?.('(prefers-color-scheme: dark)') - - function matchesMediaToColorMode(matches: boolean) { - return matches ? 'night' : 'day' - } - - function handleChange(event: MediaQueryListEvent) { - const isNight = event.matches - setSystemColorMode(matchesMediaToColorMode(isNight)) - } - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (media) { - // Only update if preference changed between useState init and effect - const currentMode = matchesMediaToColorMode(media.matches) - setSystemColorMode(prev => (prev === currentMode ? prev : currentMode)) - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (media.addEventListener !== undefined) { - media.addEventListener('change', handleChange) - return function cleanup() { - media.removeEventListener('change', handleChange) - } - } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - else if (media.addListener !== undefined) { - media.addListener(handleChange) - return function cleanup() { - media.removeListener(handleChange) - } - } - } - }, []) +function subscribeToSystemColorMode(callback: () => void) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const media = window?.matchMedia?.('(prefers-color-scheme: dark)') + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + media?.addEventListener('change', callback) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return () => media?.removeEventListener('change', callback) +} - return systemColorMode +function useSystemColorMode() { + return React.useSyncExternalStore(subscribeToSystemColorMode, getSystemColorMode, () => 'day') } function getSystemColorMode(): ColorMode { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)')?.matches) { - return 'night' - } - - return 'day' + return window?.matchMedia?.('(prefers-color-scheme: dark)')?.matches ? 'night' : 'day' } function resolveColorMode(colorMode: ColorModeWithAuto, systemColorMode: ColorMode) { diff --git a/packages/styled-react/src/components/ThemeProvider.tsx b/packages/styled-react/src/components/ThemeProvider.tsx index 8ef5bfa8ab4..98b5543108c 100644 --- a/packages/styled-react/src/components/ThemeProvider.tsx +++ b/packages/styled-react/src/components/ThemeProvider.tsx @@ -38,16 +38,29 @@ const ThemeContext = React.createContext<{ }) // inspired from __NEXT_DATA__, we use application/json to avoid CSRF policy with inline scripts +const serverHandoffCache = new Map>() const getServerHandoff = (id: string) => { + const cached = serverHandoffCache.get(id) + if (cached !== undefined) return cached + try { const serverData = document.getElementById(`__PRIMER_DATA_${id}__`)?.textContent - if (serverData) return JSON.parse(serverData) + if (serverData) { + const parsed = JSON.parse(serverData) + serverHandoffCache.set(id, parsed) + return parsed + } } catch (_error) { // if document/element does not exist or JSON is invalid, supress error } - return {} + + const empty = {} + serverHandoffCache.set(id, empty) + return empty } +const emptySubscribe = () => () => {} + export const ThemeProvider: React.FC> = ({children, ...props}) => { // Get fallback values from parent ThemeProvider (if exists) const { @@ -61,32 +74,25 @@ export const ThemeProvider: React.FC const theme = props.theme ?? fallbackTheme ?? defaultTheme const uniqueDataId = useId() - // Lazy initializer reads DOM + parses JSON once instead of every render - const [serverColorMode, setServerColorMode] = React.useState( - () => getServerHandoff(uniqueDataId).resolvedServerColorMode, - ) const [colorMode, setColorMode] = useSyncedState(props.colorMode ?? fallbackColorMode ?? defaultColorMode) const [dayScheme, setDayScheme] = useSyncedState(props.dayScheme ?? fallbackDayScheme ?? defaultDayScheme) const [nightScheme, setNightScheme] = useSyncedState(props.nightScheme ?? fallbackNightScheme ?? defaultNightScheme) const systemColorMode = useSystemColorMode() - const resolvedColorMode = serverColorMode ?? resolveColorMode(colorMode, systemColorMode) + const clientColorMode = resolveColorMode(colorMode, systemColorMode) + // During SSR/hydration, use the server-rendered color mode from the handoff script tag + // to avoid mismatches. After hydration, resolve from client state. + const resolvedColorMode = React.useSyncExternalStore( + emptySubscribe, + () => clientColorMode, + () => getServerHandoff(uniqueDataId).resolvedServerColorMode ?? clientColorMode, + ) const colorScheme = chooseColorScheme(resolvedColorMode, dayScheme, nightScheme) const {resolvedTheme, resolvedColorScheme} = React.useMemo( () => applyColorScheme(theme, colorScheme), [theme, colorScheme], ) - // After hydration, clear the server passthrough so client-side color mode takes over - React.useEffect( - function clearServerPassthrough() { - if (serverColorMode !== undefined) { - setServerColorMode(undefined) - } - }, - [serverColorMode], - ) - const contextValue = React.useMemo( () => ({ theme: resolvedTheme, @@ -139,55 +145,22 @@ export function useColorSchemeVar(values: Partial>, fallb return values[colorScheme] ?? fallback } -function useSystemColorMode() { - const [systemColorMode, setSystemColorMode] = React.useState(getSystemColorMode) - - React.useEffect(() => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const media = window?.matchMedia?.('(prefers-color-scheme: dark)') - - function matchesMediaToColorMode(matches: boolean) { - return matches ? 'night' : 'day' - } - - function handleChange(event: MediaQueryListEvent) { - const isNight = event.matches - setSystemColorMode(matchesMediaToColorMode(isNight)) - } - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (media) { - // Only update if preference changed between useState init and effect - const currentMode = matchesMediaToColorMode(media.matches) - setSystemColorMode(prev => (prev === currentMode ? prev : currentMode)) - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (media.addEventListener !== undefined) { - media.addEventListener('change', handleChange) - return function cleanup() { - media.removeEventListener('change', handleChange) - } - } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - else if (media.addListener !== undefined) { - media.addListener(handleChange) - return function cleanup() { - media.removeListener(handleChange) - } - } - } - }, []) +function subscribeToSystemColorMode(callback: () => void) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const media = window?.matchMedia?.('(prefers-color-scheme: dark)') + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + media?.addEventListener('change', callback) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return () => media?.removeEventListener('change', callback) +} - return systemColorMode +function useSystemColorMode() { + return React.useSyncExternalStore(subscribeToSystemColorMode, getSystemColorMode, () => 'day') } function getSystemColorMode(): ColorMode { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)')?.matches) { - return 'night' - } - - return 'day' + return window?.matchMedia?.('(prefers-color-scheme: dark)')?.matches ? 'night' : 'day' } function resolveColorMode(colorMode: ColorModeWithAuto, systemColorMode: ColorMode) { From 74afdb511a0993eda0c8ff7df9a237c5c1389912 Mon Sep 17 00:00:00 2001 From: Matthew Costabile Date: Wed, 25 Mar 2026 19:12:47 +0000 Subject: [PATCH 5/5] perf(ThemeProvider): guard getServerHandoff for SSR and use stable empty object --- packages/react/src/ThemeProvider.tsx | 3 +++ packages/styled-react/src/components/ThemeProvider.tsx | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/react/src/ThemeProvider.tsx b/packages/react/src/ThemeProvider.tsx index a784626a1f6..1328813a76e 100644 --- a/packages/react/src/ThemeProvider.tsx +++ b/packages/react/src/ThemeProvider.tsx @@ -39,7 +39,10 @@ const ThemeContext = React.createContext<{ // inspired from __NEXT_DATA__, we use application/json to avoid CSRF policy with inline scripts const serverHandoffCache = new Map>() +const emptyHandoff: Record = {} const getServerHandoff = (id: string) => { + if (typeof document === 'undefined') return emptyHandoff + const cached = serverHandoffCache.get(id) if (cached !== undefined) return cached diff --git a/packages/styled-react/src/components/ThemeProvider.tsx b/packages/styled-react/src/components/ThemeProvider.tsx index 98b5543108c..8217ba446be 100644 --- a/packages/styled-react/src/components/ThemeProvider.tsx +++ b/packages/styled-react/src/components/ThemeProvider.tsx @@ -39,7 +39,10 @@ const ThemeContext = React.createContext<{ // inspired from __NEXT_DATA__, we use application/json to avoid CSRF policy with inline scripts const serverHandoffCache = new Map>() +const emptyHandoff: Record = {} const getServerHandoff = (id: string) => { + if (typeof document === 'undefined') return emptyHandoff + const cached = serverHandoffCache.get(id) if (cached !== undefined) return cached