UNPKG

3.35 kBPlain TextView Raw
1import React from 'react'
2import matchMedia from 'matchmediaquery'
3import hyphenate from 'hyphenate-style-name'
4import { shallowEqualObjects } from 'shallow-equal'
5import toQuery from './toQuery'
6import Context from './Context'
7import { MediaQueryAllQueryable, MediaQueryMatchers } from './types'
8
9type MediaQuerySettings = Partial<MediaQueryAllQueryable & { query?: string }>
10
11const makeQuery = (settings: Record<string, any>) => settings.query || toQuery(settings)
12
13const hyphenateKeys = (obj?: Record<string, any>): Record<string, any> | null => {
14 if (!obj) return null
15 const keys = Object.keys(obj)
16 if (keys.length === 0) return null
17
18 return keys.reduce((result, key) => {
19 result[hyphenate(key)] = obj[key]
20 return result
21 }, {} as Record<string, any>)
22}
23
24const useIsUpdate = () => {
25 const ref = React.useRef(false)
26
27 React.useEffect(() => {
28 ref.current = true
29 }, [])
30
31 return ref.current
32}
33
34const useDevice = (deviceFromProps?: MediaQueryMatchers): Partial<MediaQueryAllQueryable> => {
35 const deviceFromContext = React.useContext(Context)
36 const getDevice = () =>
37 hyphenateKeys(deviceFromProps) || hyphenateKeys(deviceFromContext) || {}
38 const [ device, setDevice ] = React.useState(getDevice)
39
40 React.useEffect(() => {
41 const newDevice = getDevice()
42 if (!shallowEqualObjects(device, newDevice)) {
43 setDevice(newDevice)
44 }
45 }, [ deviceFromProps, deviceFromContext ])
46
47 return device
48}
49
50const useQuery = (settings: MediaQuerySettings) => {
51 const getQuery = () => makeQuery(settings)
52 const [ query, setQuery ] = React.useState(getQuery)
53
54 React.useEffect(() => {
55 const newQuery = getQuery()
56 if (query !== newQuery) {
57 setQuery(newQuery)
58 }
59 }, [ settings ])
60
61 return query
62}
63
64const useMatchMedia = (query: string, device: MediaQueryMatchers) => {
65 const getMatchMedia = () => matchMedia(query, device)
66 const [ mq, setMq ] = React.useState(getMatchMedia)
67 const isUpdate = useIsUpdate()
68
69 React.useEffect(() => {
70 if (isUpdate) {
71 // skip on mounting, it has already been set
72 const newMq = getMatchMedia();
73 setMq(newMq);
74
75 return () => {
76 if (newMq) {
77 newMq.dispose();
78 }
79 }
80 }
81 }, [ query, device ])
82
83 return mq
84}
85
86const useMatches = (mediaQuery: MediaQueryList): boolean => {
87 const [ matches, setMatches ] = React.useState<boolean>(mediaQuery.matches)
88
89 React.useEffect(() => {
90 const updateMatches = () => {
91 setMatches(mediaQuery.matches)
92 }
93 mediaQuery.addListener(updateMatches)
94 updateMatches()
95
96 return () => {
97 mediaQuery.removeListener(updateMatches)
98 }
99 }, [ mediaQuery ])
100
101 return matches
102}
103
104const useMediaQuery = (settings: MediaQuerySettings, device?: MediaQueryMatchers, onChange?: (_: boolean) => void) => {
105 const deviceSettings = useDevice(device)
106 const query = useQuery(settings)
107 if (!query) throw new Error('Invalid or missing MediaQuery!')
108 const mq = useMatchMedia(query, deviceSettings)
109 const matches = useMatches(mq as unknown as MediaQueryList)
110 const isUpdate = useIsUpdate()
111
112 React.useEffect(() => {
113 if (isUpdate && onChange) {
114 onChange(matches)
115 }
116 }, [ matches ])
117
118 React.useEffect(() => {
119 return () => {
120 if (mq) {
121 mq.dispose();
122 }
123 }
124 }, []);
125
126 return matches
127}
128
129export default useMediaQuery