Skip to content
Open
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/graduate-theming-feature-flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

ThemeProvider: Remove `primer_react_theme_provider_remove_ssr_handoff` feature flag.
1 change: 0 additions & 1 deletion packages/react/src/FeatureFlags/DefaultFeatureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ export const DefaultFeatureFlags = FeatureFlagScope.create({
primer_react_action_list_group_heading_trailing_action: false,
primer_react_action_list_item_gap: false,
primer_react_timeline_list_semantics: false,
primer_react_theme_provider_remove_ssr_handoff: false,
})
61 changes: 3 additions & 58 deletions packages/react/src/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
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'
import {ThemeContext} from './ThemeContext'
import {useTheme} from './useTheme'
Expand All @@ -21,7 +19,7 @@ export type ThemeProviderProps = {
dayScheme?: string
nightScheme?: string
/**
* No-op when the `primer_react_theme_provider_remove_ssr_handoff` feature flag is enabled.
* @deprecated This prop is no longer used and has no effect.
*/
preventSSRMismatch?: boolean
Comment thread
francinelucca marked this conversation as resolved.
/**
Expand All @@ -32,33 +30,6 @@ export type ThemeProviderProps = {
contextOnly?: boolean
}

// inspired from __NEXT_DATA__, we use application/json to avoid CSRF policy with inline scripts
const serverHandoffCache = new Map<string, Record<string, unknown>>()
const emptyHandoff: Record<string, unknown> = {}
const getServerHandoff = (id: string) => {
if (typeof document === 'undefined') return emptyHandoff

const cached = serverHandoffCache.get(id)
if (cached !== undefined) return cached

try {
const serverData = document.getElementById(`__PRIMER_DATA_${id}__`)?.textContent
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
}

const empty = {}
serverHandoffCache.set(id, empty)
return empty
}

const emptySubscribe = () => () => {}

export const ThemeProvider: React.FC<React.PropsWithChildren<ThemeProviderProps>> = ({children, ...props}) => {
// Get fallback values from parent ThemeProvider (if exists)
const {
Expand All @@ -71,22 +42,11 @@ 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)
const [dayScheme, setDayScheme] = useSyncedState(props.dayScheme ?? fallbackDayScheme ?? defaultDayScheme)
const [nightScheme, setNightScheme] = useSyncedState(props.nightScheme ?? fallbackNightScheme ?? defaultNightScheme)
const systemColorMode = useSystemColorMode()
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 ssrResolvedColorMode = React.useSyncExternalStore(
emptySubscribe,
() => clientColorMode,
() => getServerHandoff(uniqueDataId).resolvedServerColorMode ?? clientColorMode,
)
const resolvedColorMode = removeSSRHandoff ? clientColorMode : ssrResolvedColorMode
const resolvedColorMode = resolveColorMode(colorMode, systemColorMode)
const colorScheme = chooseColorScheme(resolvedColorMode, dayScheme, nightScheme)
Comment thread
francinelucca marked this conversation as resolved.
const {resolvedTheme, resolvedColorScheme} = React.useMemo(
() => applyColorScheme(theme, colorScheme),
Expand Down Expand Up @@ -120,22 +80,8 @@ export const ThemeProvider: React.FC<React.PropsWithChildren<ThemeProviderProps>
],
)

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

if (props.contextOnly) {
return (
<ThemeContext.Provider value={contextValue}>
{children}
{ssrHandoffScript}
</ThemeContext.Provider>
)
return <ThemeContext.Provider value={contextValue}>{children}</ThemeContext.Provider>
}

return (
Expand All @@ -146,7 +92,6 @@ export const ThemeProvider: React.FC<React.PropsWithChildren<ThemeProviderProps>
data-dark-theme={nightScheme}
>
{children}
{ssrHandoffScript}
</div>
</ThemeContext.Provider>
)
Expand Down
58 changes: 0 additions & 58 deletions packages/react/src/__tests__/ThemeProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import userEvent from '@testing-library/user-event'
import {describe, expect, it, vi} from 'vitest'
import React from 'react'
import ThemeProvider from '../ThemeProvider'
import {FeatureFlags} from '../FeatureFlags'
import {useColorSchemeVar, useTheme} from '../useTheme'

// copied from '@primer/primitives/dist/css/functional/themes/';
Expand Down Expand Up @@ -571,61 +570,4 @@ describe('contextOnly', () => {

expect(screen.getByTestId('consumer').textContent).toBe('night-light-dark_dimmed')
})

it('renders the preventSSRMismatch script tag when contextOnly and preventSSRMismatch are both true', () => {
const {container} = render(
<ThemeProvider contextOnly preventSSRMismatch>
<span>Hello</span>
</ThemeProvider>,
)

const div = container.querySelector('[data-color-mode]')
expect(div).not.toBeInTheDocument()

const script = container.querySelector('script[type="application/json"]')
expect(script).toBeInTheDocument()
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')
})
})
2 changes: 1 addition & 1 deletion packages/react/src/utils/useTheme.hookDocs.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
{
"name": "preventSSRMismatch",
"type": "boolean",
"description": "Prevent SSR mismatch by injecting server-side color mode."
"description": "Deprecated: This prop is no longer used and has no effect."
}
]
},
Expand Down
Loading