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/usemedia-ssr-warnings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

Add SSR warnings to useMediaUnsafeSSR and useResponsiveValue.
12 changes: 6 additions & 6 deletions packages/react/src/hooks/__tests__/useMedia.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -38,7 +38,7 @@ function mockMatchMedia({defaultMatch = false} = {}) {
}
}

describe('useMedia', () => {
describe('useMediaUnsafeSSR', () => {
afterEach(() => {
mockMatchMedia()
})
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -104,7 +104,7 @@ describe('useMedia', () => {
const match: boolean[] = []

function TestComponent() {
const value = useMedia(feature)
const value = useMediaUnsafeSSR(feature)
match.push(value)
return null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@ 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,
* this could cause a hydration mismatch between server and client.
*
* @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) {
Expand All @@ -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.',
'`useMediaUnsafeSSR` When server side rendering, defaultState should be defined to prevent a hydration mismatch.',
)

return false
Expand Down Expand Up @@ -103,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`
*
Expand Down
21 changes: 15 additions & 6 deletions packages/react/src/hooks/useResponsiveValue.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {useMedia} from './useMedia'
import {useMediaUnsafeSSR} from './useMediaUnsafeSSR'
import {canUseDOM} from '../utils/environment'
import {warning} from '../utils/warning'

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

Expand Down Expand Up @@ -41,17 +43,24 @@ export function isResponsiveValue(value: any): value is ResponsiveValue<any> {
* 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 `useMediaUnsafeSSR` 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<T, F>(value: T, fallback: F): FlattenResponsiveValue<T> | F {
// Check viewport size
// TODO: Improve SSR support
// TODO: What is the performance cost of creating media query listeners in this hook?
const isNarrowViewport = useMedia(viewportRanges.narrow, false)
const isRegularViewport = useMedia(viewportRanges.regular, false)
const isWideViewport = useMedia(viewportRanges.wide, false)
// Check viewport size
const isNarrowViewport = useMediaUnsafeSSR(viewportRanges.narrow, false)
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
Expand Down
Loading