From d7da8b01269970de95994af50f495fca1d45b793 Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Fri, 24 Oct 2025 13:44:58 +0000 Subject: [PATCH 1/5] Add warnings --- packages/react/src/hooks/useMedia.tsx | 5 ++++- packages/react/src/hooks/useResponsiveValue.ts | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/react/src/hooks/useMedia.tsx b/packages/react/src/hooks/useMedia.tsx index a394bffa36a..011e2de099c 100644 --- a/packages/react/src/hooks/useMedia.tsx +++ b/packages/react/src/hooks/useMedia.tsx @@ -9,6 +9,9 @@ import {warning} from '../utils/warning' * If `MatchMedia` is used as an ancestor, `useMedia` will instead use the * value of the media query string, if available * + * Warning: If rendering on the server, and no `defaultState` is provided, + * this could cause a hydration mismatch between server and client. + * * @example * function Example() { * const coarsePointer = useMedia('(pointer: coarse)'); @@ -34,7 +37,7 @@ export function useMedia(mediaQueryString: string, defaultState?: boolean) { // A default value has not been provided, and you are rendering on the server, warn of a possible hydration mismatch when defaulting to false. warning( true, - '`useMedia` When server side rendering, defaultState should be defined to prevent a hydration mismatches.', + '`useMedia` When server side rendering, defaultState should be defined to prevent a hydration mismatch.', ) return false diff --git a/packages/react/src/hooks/useResponsiveValue.ts b/packages/react/src/hooks/useResponsiveValue.ts index 7a712117b54..c00ce425d3b 100644 --- a/packages/react/src/hooks/useResponsiveValue.ts +++ b/packages/react/src/hooks/useResponsiveValue.ts @@ -41,14 +41,16 @@ export function isResponsiveValue(value: any): value is ResponsiveValue { * Resolves responsive values based on the current viewport width. * For example, if the current viewport width is narrow (less than 768px), the value of `{regular: 'foo', narrow: 'bar'}` will resolve to `'bar'`. * + * Warning: This hook is not fully SSR compatible as it relies on `useMedia` without a `defaultState`. Using `getResponsiveAttributes` is preferred to avoid hydration mismatches. + * * @example * const value = useResponsiveValue({regular: 'foo', narrow: 'bar'}) * console.log(value) // 'bar' */ -// TODO: Improve SSR support export function useResponsiveValue(value: T, fallback: F): FlattenResponsiveValue | F { - // Check viewport size + // TODO: Improve SSR support // TODO: What is the performance cost of creating media query listeners in this hook? + // Check viewport size const isNarrowViewport = useMedia(viewportRanges.narrow, false) const isRegularViewport = useMedia(viewportRanges.regular, false) const isWideViewport = useMedia(viewportRanges.wide, false) From 84ffeaa5440ce093d5503a45fd772b524a139869 Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Fri, 24 Oct 2025 13:51:00 +0000 Subject: [PATCH 2/5] Rename useMedia --- packages/react/src/hooks/__tests__/useMedia.test.tsx | 12 ++++++------ .../hooks/{useMedia.tsx => useMediaUnsafeSSR.tsx} | 12 ++++++------ packages/react/src/hooks/useResponsiveValue.ts | 10 +++++----- 3 files changed, 17 insertions(+), 17 deletions(-) rename packages/react/src/hooks/{useMedia.tsx => useMediaUnsafeSSR.tsx} (90%) diff --git a/packages/react/src/hooks/__tests__/useMedia.test.tsx b/packages/react/src/hooks/__tests__/useMedia.test.tsx index 2884c18f2da..313bea7fa06 100644 --- a/packages/react/src/hooks/__tests__/useMedia.test.tsx +++ b/packages/react/src/hooks/__tests__/useMedia.test.tsx @@ -2,7 +2,7 @@ import {render} from '@testing-library/react' import {afterEach, describe, expect, it, vi} from 'vitest' import {act} from 'react' import ReactDOM from 'react-dom/server' -import {useMedia, MatchMedia} from '../useMedia' +import {useMediaUnsafeSSR, MatchMedia} from '../useMediaUnsafeSSR' type MediaQueryEventListener = (event: {matches: boolean}) => void @@ -38,7 +38,7 @@ function mockMatchMedia({defaultMatch = false} = {}) { } } -describe('useMedia', () => { +describe('useMediaUnsafeSSR', () => { afterEach(() => { mockMatchMedia() }) @@ -49,7 +49,7 @@ describe('useMedia', () => { const match: boolean[] = [] function TestComponent() { - const value = useMedia('(pointer: coarse)') + const value = useMediaUnsafeSSR('(pointer: coarse)') match.push(value) return null } @@ -67,7 +67,7 @@ describe('useMedia', () => { const match: boolean[] = [] function TestComponent() { - const value = useMedia('(pointer: coarse)') + const value = useMediaUnsafeSSR('(pointer: coarse)') match.push(value) return null } @@ -82,7 +82,7 @@ describe('useMedia', () => { const match: boolean[] = [] function TestComponent() { - const value = useMedia('(pointer: coarse)') + const value = useMediaUnsafeSSR('(pointer: coarse)') match.push(value) return null } @@ -104,7 +104,7 @@ describe('useMedia', () => { const match: boolean[] = [] function TestComponent() { - const value = useMedia(feature) + const value = useMediaUnsafeSSR(feature) match.push(value) return null } diff --git a/packages/react/src/hooks/useMedia.tsx b/packages/react/src/hooks/useMediaUnsafeSSR.tsx similarity index 90% rename from packages/react/src/hooks/useMedia.tsx rename to packages/react/src/hooks/useMediaUnsafeSSR.tsx index 011e2de099c..e7a05017dfa 100644 --- a/packages/react/src/hooks/useMedia.tsx +++ b/packages/react/src/hooks/useMediaUnsafeSSR.tsx @@ -3,10 +3,10 @@ import {canUseDOM} from '../utils/environment' import {warning} from '../utils/warning' /** - * `useMedia` will use the given `mediaQueryString` with `matchMedia` to + * `useMediaUnsafeSSR` will use the given `mediaQueryString` with `matchMedia` to * determine if the document matches the media query string. * - * If `MatchMedia` is used as an ancestor, `useMedia` will instead use the + * If `MatchMedia` is used as an ancestor, `useMediaUnsafeSSR` will instead use the * value of the media query string, if available * * Warning: If rendering on the server, and no `defaultState` is provided, @@ -14,11 +14,11 @@ import {warning} from '../utils/warning' * * @example * function Example() { - * const coarsePointer = useMedia('(pointer: coarse)'); + * const coarsePointer = useMediaUnsafeSSR('(pointer: coarse)'); * // ... * } */ -export function useMedia(mediaQueryString: string, defaultState?: boolean) { +export function useMediaUnsafeSSR(mediaQueryString: string, defaultState?: boolean) { const features = useContext(MatchMediaContext) const [matches, setMatches] = React.useState(() => { if (features[mediaQueryString] !== undefined) { @@ -37,7 +37,7 @@ export function useMedia(mediaQueryString: string, defaultState?: boolean) { // A default value has not been provided, and you are rendering on the server, warn of a possible hydration mismatch when defaulting to false. warning( true, - '`useMedia` When server side rendering, defaultState should be defined to prevent a hydration mismatch.', + '`useMediaUnsafeSSR` When server side rendering, defaultState should be defined to prevent a hydration mismatch.', ) return false @@ -106,7 +106,7 @@ const defaultFeatures = {} /** * Use `MatchMedia` to emulate media conditions by passing in feature - * queries to the `features` prop. If a component uses `useMedia` with the + * queries to the `features` prop. If a component uses `useMediaUnsafeSSR` with the * feature passed in to `MatchMedia` it will force its value to match what is * provided to `MatchMedia` * diff --git a/packages/react/src/hooks/useResponsiveValue.ts b/packages/react/src/hooks/useResponsiveValue.ts index c00ce425d3b..b65d833e601 100644 --- a/packages/react/src/hooks/useResponsiveValue.ts +++ b/packages/react/src/hooks/useResponsiveValue.ts @@ -1,4 +1,4 @@ -import {useMedia} from './useMedia' +import {useMediaUnsafeSSR} from './useMediaUnsafeSSR' // This file contains utilities for working with responsive values. @@ -41,7 +41,7 @@ export function isResponsiveValue(value: any): value is ResponsiveValue { * Resolves responsive values based on the current viewport width. * For example, if the current viewport width is narrow (less than 768px), the value of `{regular: 'foo', narrow: 'bar'}` will resolve to `'bar'`. * - * Warning: This hook is not fully SSR compatible as it relies on `useMedia` without a `defaultState`. Using `getResponsiveAttributes` is preferred to avoid hydration mismatches. + * Warning: This hook is not fully SSR compatible as it relies on `useMediaUnsafeSSR` without a `defaultState`. Using `getResponsiveAttributes` is preferred to avoid hydration mismatches. * * @example * const value = useResponsiveValue({regular: 'foo', narrow: 'bar'}) @@ -51,9 +51,9 @@ export function useResponsiveValue(value: T, fallback: F): FlattenResponsi // TODO: Improve SSR support // TODO: What is the performance cost of creating media query listeners in this hook? // Check viewport size - const isNarrowViewport = useMedia(viewportRanges.narrow, false) - const isRegularViewport = useMedia(viewportRanges.regular, false) - const isWideViewport = useMedia(viewportRanges.wide, false) + const isNarrowViewport = useMediaUnsafeSSR(viewportRanges.narrow, false) + const isRegularViewport = useMediaUnsafeSSR(viewportRanges.regular, false) + const isWideViewport = useMediaUnsafeSSR(viewportRanges.wide, false) if (isResponsiveValue(value)) { // If we've reached this line, we know that value is a responsive value From 3714548b2803abf9a17bf4907e60c6903cfd7c23 Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Tue, 28 Oct 2025 15:56:47 +0000 Subject: [PATCH 3/5] Add warning to useResponsiveValue --- packages/react/src/hooks/useResponsiveValue.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/react/src/hooks/useResponsiveValue.ts b/packages/react/src/hooks/useResponsiveValue.ts index b65d833e601..6bb2bea1d2f 100644 --- a/packages/react/src/hooks/useResponsiveValue.ts +++ b/packages/react/src/hooks/useResponsiveValue.ts @@ -1,4 +1,6 @@ import {useMediaUnsafeSSR} from './useMediaUnsafeSSR' +import {canUseDOM} from '../utils/environment' +import {warning} from '../utils/warning' // This file contains utilities for working with responsive values. @@ -55,6 +57,11 @@ export function useResponsiveValue(value: T, fallback: F): FlattenResponsi const isRegularViewport = useMediaUnsafeSSR(viewportRanges.regular, false) const isWideViewport = useMediaUnsafeSSR(viewportRanges.wide, false) + warning( + !canUseDOM, + '`useResponsiveValue` is not fully SSR compatible as it relies on `useMediaUnsafeSSR` without a `defaultState`. Using `getResponsiveAttributes` is preferred to avoid hydration mismatches.', + ) + if (isResponsiveValue(value)) { // If we've reached this line, we know that value is a responsive value // eslint-disable-next-line @typescript-eslint/no-explicit-any From ed82b10bcc206ba17648b64844862e0334bed9f8 Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Tue, 28 Oct 2025 16:51:34 +0000 Subject: [PATCH 4/5] Add changeset --- .changeset/usemedia-ssr-warnings.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/usemedia-ssr-warnings.md diff --git a/.changeset/usemedia-ssr-warnings.md b/.changeset/usemedia-ssr-warnings.md new file mode 100644 index 00000000000..c1940f80c16 --- /dev/null +++ b/.changeset/usemedia-ssr-warnings.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Add SSR warnings to useMediaUnsafeSSR and useResponsiveValue. From f5964dd972f4451a9af63437f9e06f490e386e76 Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Fri, 31 Oct 2025 10:46:31 +0100 Subject: [PATCH 5/5] Update .changeset/usemedia-ssr-warnings.md Co-authored-by: Jon Rohan --- .changeset/usemedia-ssr-warnings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/usemedia-ssr-warnings.md b/.changeset/usemedia-ssr-warnings.md index c1940f80c16..15dd0bbf852 100644 --- a/.changeset/usemedia-ssr-warnings.md +++ b/.changeset/usemedia-ssr-warnings.md @@ -1,5 +1,5 @@ --- -'@primer/react': patch +'@primer/react': minor --- Add SSR warnings to useMediaUnsafeSSR and useResponsiveValue.