Skip to content

Commit f7dfa7d

Browse files
hectahertzjonrohan
andauthored
Add SSR warnings on useMedia/useResponsiveValue (#7070)
Co-authored-by: Jon Rohan <yes@jonrohan.codes>
1 parent e3f1da0 commit f7dfa7d

File tree

4 files changed

+35
-18
lines changed

4 files changed

+35
-18
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': minor
3+
---
4+
5+
Add SSR warnings to useMediaUnsafeSSR and useResponsiveValue.

packages/react/src/hooks/__tests__/useMedia.test.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {render} from '@testing-library/react'
22
import {afterEach, describe, expect, it, vi} from 'vitest'
33
import {act} from 'react'
44
import ReactDOM from 'react-dom/server'
5-
import {useMedia, MatchMedia} from '../useMedia'
5+
import {useMediaUnsafeSSR, MatchMedia} from '../useMediaUnsafeSSR'
66

77
type MediaQueryEventListener = (event: {matches: boolean}) => void
88

@@ -38,7 +38,7 @@ function mockMatchMedia({defaultMatch = false} = {}) {
3838
}
3939
}
4040

41-
describe('useMedia', () => {
41+
describe('useMediaUnsafeSSR', () => {
4242
afterEach(() => {
4343
mockMatchMedia()
4444
})
@@ -49,7 +49,7 @@ describe('useMedia', () => {
4949
const match: boolean[] = []
5050

5151
function TestComponent() {
52-
const value = useMedia('(pointer: coarse)')
52+
const value = useMediaUnsafeSSR('(pointer: coarse)')
5353
match.push(value)
5454
return null
5555
}
@@ -67,7 +67,7 @@ describe('useMedia', () => {
6767
const match: boolean[] = []
6868

6969
function TestComponent() {
70-
const value = useMedia('(pointer: coarse)')
70+
const value = useMediaUnsafeSSR('(pointer: coarse)')
7171
match.push(value)
7272
return null
7373
}
@@ -82,7 +82,7 @@ describe('useMedia', () => {
8282
const match: boolean[] = []
8383

8484
function TestComponent() {
85-
const value = useMedia('(pointer: coarse)')
85+
const value = useMediaUnsafeSSR('(pointer: coarse)')
8686
match.push(value)
8787
return null
8888
}
@@ -104,7 +104,7 @@ describe('useMedia', () => {
104104
const match: boolean[] = []
105105

106106
function TestComponent() {
107-
const value = useMedia(feature)
107+
const value = useMediaUnsafeSSR(feature)
108108
match.push(value)
109109
return null
110110
}

packages/react/src/hooks/useMedia.tsx renamed to packages/react/src/hooks/useMediaUnsafeSSR.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,22 @@ import {canUseDOM} from '../utils/environment'
33
import {warning} from '../utils/warning'
44

55
/**
6-
* `useMedia` will use the given `mediaQueryString` with `matchMedia` to
6+
* `useMediaUnsafeSSR` will use the given `mediaQueryString` with `matchMedia` to
77
* determine if the document matches the media query string.
88
*
9-
* If `MatchMedia` is used as an ancestor, `useMedia` will instead use the
9+
* If `MatchMedia` is used as an ancestor, `useMediaUnsafeSSR` will instead use the
1010
* value of the media query string, if available
1111
*
12+
* Warning: If rendering on the server, and no `defaultState` is provided,
13+
* this could cause a hydration mismatch between server and client.
14+
*
1215
* @example
1316
* function Example() {
14-
* const coarsePointer = useMedia('(pointer: coarse)');
17+
* const coarsePointer = useMediaUnsafeSSR('(pointer: coarse)');
1518
* // ...
1619
* }
1720
*/
18-
export function useMedia(mediaQueryString: string, defaultState?: boolean) {
21+
export function useMediaUnsafeSSR(mediaQueryString: string, defaultState?: boolean) {
1922
const features = useContext(MatchMediaContext)
2023
const [matches, setMatches] = React.useState(() => {
2124
if (features[mediaQueryString] !== undefined) {
@@ -34,7 +37,7 @@ export function useMedia(mediaQueryString: string, defaultState?: boolean) {
3437
// A default value has not been provided, and you are rendering on the server, warn of a possible hydration mismatch when defaulting to false.
3538
warning(
3639
true,
37-
'`useMedia` When server side rendering, defaultState should be defined to prevent a hydration mismatches.',
40+
'`useMediaUnsafeSSR` When server side rendering, defaultState should be defined to prevent a hydration mismatch.',
3841
)
3942

4043
return false
@@ -103,7 +106,7 @@ const defaultFeatures = {}
103106

104107
/**
105108
* Use `MatchMedia` to emulate media conditions by passing in feature
106-
* queries to the `features` prop. If a component uses `useMedia` with the
109+
* queries to the `features` prop. If a component uses `useMediaUnsafeSSR` with the
107110
* feature passed in to `MatchMedia` it will force its value to match what is
108111
* provided to `MatchMedia`
109112
*

packages/react/src/hooks/useResponsiveValue.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import {useMedia} from './useMedia'
1+
import {useMediaUnsafeSSR} from './useMediaUnsafeSSR'
2+
import {canUseDOM} from '../utils/environment'
3+
import {warning} from '../utils/warning'
24

35
// This file contains utilities for working with responsive values.
46

@@ -41,17 +43,24 @@ export function isResponsiveValue(value: any): value is ResponsiveValue<any> {
4143
* Resolves responsive values based on the current viewport width.
4244
* For example, if the current viewport width is narrow (less than 768px), the value of `{regular: 'foo', narrow: 'bar'}` will resolve to `'bar'`.
4345
*
46+
* Warning: This hook is not fully SSR compatible as it relies on `useMediaUnsafeSSR` without a `defaultState`. Using `getResponsiveAttributes` is preferred to avoid hydration mismatches.
47+
*
4448
* @example
4549
* const value = useResponsiveValue({regular: 'foo', narrow: 'bar'})
4650
* console.log(value) // 'bar'
4751
*/
48-
// TODO: Improve SSR support
4952
export function useResponsiveValue<T, F>(value: T, fallback: F): FlattenResponsiveValue<T> | F {
50-
// Check viewport size
53+
// TODO: Improve SSR support
5154
// TODO: What is the performance cost of creating media query listeners in this hook?
52-
const isNarrowViewport = useMedia(viewportRanges.narrow, false)
53-
const isRegularViewport = useMedia(viewportRanges.regular, false)
54-
const isWideViewport = useMedia(viewportRanges.wide, false)
55+
// Check viewport size
56+
const isNarrowViewport = useMediaUnsafeSSR(viewportRanges.narrow, false)
57+
const isRegularViewport = useMediaUnsafeSSR(viewportRanges.regular, false)
58+
const isWideViewport = useMediaUnsafeSSR(viewportRanges.wide, false)
59+
60+
warning(
61+
!canUseDOM,
62+
'`useResponsiveValue` is not fully SSR compatible as it relies on `useMediaUnsafeSSR` without a `defaultState`. Using `getResponsiveAttributes` is preferred to avoid hydration mismatches.',
63+
)
5564

5665
if (isResponsiveValue(value)) {
5766
// If we've reached this line, we know that value is a responsive value

0 commit comments

Comments
 (0)