Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/remove-ssr-handoff-script-under-ff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

ThemeProvider: Skip rendering the SSR handoff script when the `primer_react_theme_provider_remove_ssr_handoff` feature flag is enabled
1 change: 1 addition & 0 deletions packages/react/src/FeatureFlags/DefaultFeatureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export const DefaultFeatureFlags = FeatureFlagScope.create({
primer_react_styled_react_use_primer_theme_providers: false,
primer_react_action_list_group_heading_trailing_action: false,
primer_react_timeline_list_semantics: false,
primer_react_theme_provider_remove_ssr_handoff: false,
})
23 changes: 15 additions & 8 deletions packages/react/src/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react'
import defaultTheme from './theme'
import deepmerge from 'deepmerge'
import {useId} from './hooks'
import {useFeatureFlag} from './FeatureFlags'
import {useSyncedState} from './hooks/useSyncedState'

export const defaultColorMode = 'day'
Expand All @@ -17,6 +18,9 @@ export type ThemeProviderProps = {
colorMode?: ColorModeWithAuto
dayScheme?: string
nightScheme?: string
/**
* No-op when the `primer_react_theme_provider_remove_ssr_handoff` feature flag is enabled.
*/
preventSSRMismatch?: boolean
/**
* When true, only provides theme context to descendants without rendering
Expand Down Expand Up @@ -82,6 +86,7 @@ export const ThemeProvider: React.FC<React.PropsWithChildren<ThemeProviderProps>
// Initialize state
const theme = fallbackTheme ?? defaultTheme

const removeSSRHandoff = useFeatureFlag('primer_react_theme_provider_remove_ssr_handoff')
const uniqueDataId = useId()

const [colorMode, setColorMode] = useSyncedState(props.colorMode ?? fallbackColorMode ?? defaultColorMode)
Expand All @@ -91,11 +96,12 @@ export const ThemeProvider: React.FC<React.PropsWithChildren<ThemeProviderProps>
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(
const ssrResolvedColorMode = React.useSyncExternalStore(
emptySubscribe,
() => clientColorMode,
() => getServerHandoff(uniqueDataId).resolvedServerColorMode ?? clientColorMode,
)
const resolvedColorMode = removeSSRHandoff ? clientColorMode : ssrResolvedColorMode
const colorScheme = chooseColorScheme(resolvedColorMode, dayScheme, nightScheme)
const {resolvedTheme, resolvedColorScheme} = React.useMemo(
() => applyColorScheme(theme, colorScheme),
Expand Down Expand Up @@ -129,13 +135,14 @@ export const ThemeProvider: React.FC<React.PropsWithChildren<ThemeProviderProps>
],
)

const ssrHandoffScript = props.preventSSRMismatch ? (
<script
type="application/json"
id={`__PRIMER_DATA_${uniqueDataId}__`}
dangerouslySetInnerHTML={{__html: JSON.stringify({resolvedServerColorMode: resolvedColorMode})}}
/>
) : null
const ssrHandoffScript =
!removeSSRHandoff && props.preventSSRMismatch ? (
<script
type="application/json"
id={`__PRIMER_DATA_${uniqueDataId}__`}
dangerouslySetInnerHTML={{__html: JSON.stringify({resolvedServerColorMode: resolvedColorMode})}}
/>
) : null

if (props.contextOnly) {
return (
Expand Down
43 changes: 43 additions & 0 deletions packages/react/src/__tests__/ThemeProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event'
import {describe, expect, it, vi} from 'vitest'
import React from 'react'
import {ThemeProvider, useColorSchemeVar, useTheme} from '../ThemeProvider'
import {FeatureFlags} from '../FeatureFlags'

// copied from '@primer/primitives/dist/css/functional/themes/';
const fgDefaultColors = {
Expand Down Expand Up @@ -585,3 +586,45 @@ describe('contextOnly', () => {
expect(script?.textContent).toContain('resolvedServerColorMode')
})
})

describe('primer_react_theme_provider_remove_ssr_handoff feature flag', () => {
it('does not render the script tag when the feature flag is enabled', () => {
const {container} = render(
<FeatureFlags flags={{primer_react_theme_provider_remove_ssr_handoff: true}}>
<ThemeProvider preventSSRMismatch>
<span>Hello</span>
</ThemeProvider>
</FeatureFlags>,
)

const script = container.querySelector('script[type="application/json"]')
expect(script).not.toBeInTheDocument()
})

it('does not render the script tag when the feature flag is enabled and contextOnly is true', () => {
const {container} = render(
<FeatureFlags flags={{primer_react_theme_provider_remove_ssr_handoff: true}}>
<ThemeProvider contextOnly preventSSRMismatch>
<span>Hello</span>
</ThemeProvider>
</FeatureFlags>,
)

const script = container.querySelector('script[type="application/json"]')
expect(script).not.toBeInTheDocument()
})

it('renders the script tag when the feature flag is disabled', () => {
const {container} = render(
<FeatureFlags flags={{primer_react_theme_provider_remove_ssr_handoff: false}}>
<ThemeProvider preventSSRMismatch>
<span>Hello</span>
</ThemeProvider>
</FeatureFlags>,
)

const script = container.querySelector('script[type="application/json"]')
expect(script).toBeInTheDocument()
expect(script?.textContent).toContain('resolvedServerColorMode')
})
})
Loading