1 | import React from 'react'
|
2 | import matchMedia from 'matchmediaquery'
|
3 | import hyphenate from 'hyphenate-style-name'
|
4 | import { shallowEqualObjects } from 'shallow-equal'
|
5 | import toQuery from './toQuery'
|
6 | import Context from './Context'
|
7 | import { MediaQueryAllQueryable, MediaQueryMatchers } from './types'
|
8 |
|
9 | type MediaQuerySettings = Partial<MediaQueryAllQueryable & { query?: string }>
|
10 |
|
11 | const makeQuery = (settings: Record<string, any>) => settings.query || toQuery(settings)
|
12 |
|
13 | const 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 |
|
24 | const useIsUpdate = () => {
|
25 | const ref = React.useRef(false)
|
26 |
|
27 | React.useEffect(() => {
|
28 | ref.current = true
|
29 | }, [])
|
30 |
|
31 | return ref.current
|
32 | }
|
33 |
|
34 | const 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 |
|
50 | const 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 |
|
64 | const 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 |
|
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 |
|
86 | const 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 |
|
104 | const 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 |
|
129 | export default useMediaQuery
|