import { isEqual } from '@o/fast-compare' import { debounce } from 'lodash' import React, { DependencyList, EffectCallback } from 'react' const { useState, useEffect, useLayoutEffect } = React type MediaQueryObject = { [key: string]: string | number | boolean } const camelToHyphen = (str: string) => str.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`).toLowerCase() const objectToString = (query: string | MediaQueryObject) => { if (typeof query === 'string') return query return Object.entries(query) .map(([feature, value]) => { feature = camelToHyphen(feature) if (typeof value === 'boolean') { return value ? feature : `not ${feature}` } if (typeof value === 'number' && /[height|width]$/.test(feature)) { value = `${value}px` } return `(${feature}: ${value})` }) .join(' and ') } type MediaQueryShort = string | MediaQueryObject export type UseMediaOptions = { onChange?: (val?: A extends any[] ? boolean[] : boolean) => any } // use array if given array const normalizeState = (queryState: boolean[], originalQueries: any) => { return Array.isArray(originalQueries) ? queryState : queryState[0] } type EitherEffect = (effect: EffectCallback, deps?: DependencyList) => void const createUseMedia = (effect: EitherEffect) => function useMedia( rawQueries: A, options?: UseMediaOptions, ): A extends any[] ? boolean[] : boolean { const allQueries = [].concat(rawQueries) const queries = allQueries.map(objectToString) const [state, setState] = useState( normalizeState(queries.map(query => !!window.matchMedia(query).matches), rawQueries), ) effect(() => { let mounted = true const mqls = queries.map(query => window.matchMedia(query)) let last const update = () => { const next = normalizeState(mqls.map(x => !!x.matches), rawQueries) if (!isEqual(next, last)) { last = next if (options && options.onChange) { options.onChange(next as any) } else { setState(next) } } } const onChange = debounce(() => { if (!mounted) return update() }) mqls.forEach(mql => mql.addListener(onChange)) update() return () => { mounted = false mqls.forEach(x => x.removeListener(onChange)) } }, [JSON.stringify(rawQueries)]) return state as any } export const useMedia = createUseMedia(useEffect) export const useMediaLayout = createUseMedia(useLayoutEffect)