1- import { useCallback , useContext , useSyncExternalStore } from 'react'
1+ import React , { useContext , useEffect } from 'react'
2+ import { canUseDOM } from '../utils/environment'
23import { warning } from '../utils/warning'
34import { MatchMediaContext } from './MatchMediaContext'
45
@@ -17,69 +18,71 @@ import {MatchMediaContext} from './MatchMediaContext'
1718 */
1819export function useMedia ( mediaQueryString : string , defaultState ?: boolean ) {
1920 const features = useContext ( MatchMediaContext )
20- // When the query is provided through `MatchMedia` context, that value always
21- // wins and there is nothing external to subscribe to.
22- const contextValue = features [ mediaQueryString ] as boolean | undefined
21+ const [ matches , setMatches ] = React . useState ( ( ) => {
22+ if ( features [ mediaQueryString ] !== undefined ) {
23+ return features [ mediaQueryString ] as boolean
24+ }
2325
24- const subscribe = useCallback (
25- ( onStoreChange : ( ) => void ) => {
26- if ( contextValue !== undefined ) {
27- return ( ) => { }
28- }
26+ // Prevent a React hydration mismatch when a default value is provided by not defaulting to window.matchMedia(query).matches.
27+ if ( defaultState !== undefined ) {
28+ return defaultState
29+ }
2930
30- const mediaQueryList = window . matchMedia ( mediaQueryString )
31+ if ( canUseDOM ) {
32+ return window . matchMedia ( mediaQueryString ) . matches
33+ }
3134
32- // Support fallback to `addListener` for broader browser support
33- // @ts -ignore this is not present in Safari <14
34- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
35- if ( mediaQueryList . addEventListener ) {
36- mediaQueryList . addEventListener ( 'change' , onStoreChange )
37- return ( ) => {
38- mediaQueryList . removeEventListener ( 'change' , onStoreChange )
39- }
40- }
35+ // A default value has not been provided, and you are rendering on the server, warn of a possible hydration mismatch when defaulting to false.
36+ warning (
37+ true ,
38+ '`useMedia` When server side rendering, defaultState should be defined to prevent a hydration mismatches.' ,
39+ )
4140
42- mediaQueryList . addListener ( onStoreChange )
43- return ( ) => {
44- mediaQueryList . removeListener ( onStoreChange )
45- }
46- } ,
47- [ contextValue , mediaQueryString ] ,
48- )
41+ return false
42+ } )
43+
44+ if ( features [ mediaQueryString ] !== undefined && matches !== features [ mediaQueryString ] ) {
45+ setMatches ( features [ mediaQueryString ] as boolean )
46+ }
4947
50- const getSnapshot = useCallback ( ( ) => {
51- if ( contextValue !== undefined ) {
52- return contextValue
48+ useEffect ( ( ) => {
49+ // If `mediaQueryString` is present in features through `context` defer to
50+ // the value present instead of checking with matchMedia
51+ if ( features [ mediaQueryString ] !== undefined ) {
52+ return
5353 }
54- return window . matchMedia ( mediaQueryString ) . matches
55- } , [ contextValue , mediaQueryString ] )
5654
57- const getServerSnapshot = useCallback ( ( ) => {
58- if ( contextValue !== undefined ) {
59- return contextValue
55+ function listener ( event : MediaQueryListEvent ) {
56+ setMatches ( event . matches )
6057 }
6158
62- // Prevent a React hydration mismatch when a default value is provided by not
63- // defaulting to `window.matchMedia(query).matches`.
64- if ( defaultState !== undefined ) {
65- return defaultState
59+ const mediaQueryList = window . matchMedia ( mediaQueryString )
60+
61+ // Support fallback to `addListener` for broader browser support
62+ // @ts -ignore this is not present in Safari <14
63+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
64+ if ( mediaQueryList . addEventListener ) {
65+ mediaQueryList . addEventListener ( 'change' , listener )
66+ } else {
67+ mediaQueryList . addListener ( listener )
6668 }
6769
68- // A default value has not been provided, and you are rendering on the
69- // server, warn of a possible hydration mismatch when defaulting to false.
70- // `getServerSnapshot` also runs on the client during hydration; only warn on
71- // the actual server so we don't surface this message in the browser console
72- // (matching the previous behavior, where the client read `matchMedia` and
73- // stayed quiet).
74- warning (
75- typeof window === 'undefined' ,
76- '`useMedia` When server side rendering, defaultState should be defined to prevent a hydration mismatches.' ,
77- )
70+ // Make sure the media query list is in sync with the matches state
71+ // eslint-disable-next-line react-hooks/set-state-in-effect, react-you-might-not-need-an-effect/no-external-store-subscription
72+ setMatches ( mediaQueryList . matches )
7873
79- return false
80- } , [ contextValue , defaultState ] )
74+ return ( ) => {
75+ // @ts -ignore this is not present in Safari <14
76+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
77+ if ( mediaQueryList . addEventListener ) {
78+ mediaQueryList . removeEventListener ( 'change' , listener )
79+ } else {
80+ mediaQueryList . removeListener ( listener )
81+ }
82+ }
83+ } , [ features , mediaQueryString ] )
8184
82- return useSyncExternalStore ( subscribe , getSnapshot , getServerSnapshot )
85+ return matches
8386}
8487
8588export { MatchMedia } from './MatchMedia'
0 commit comments