Last active
October 11, 2022 03:41
-
-
Save petamoriken/070878457954f05a1f54e8491b9b1fd9 to your computer and use it in GitHub Desktop.
雑に Responsive Design 用の React Custom Hooks を作ったやつ。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useEffect, useMemo, useState } from "react"; | |
/** | |
* 後続の Iterator Value と合わせた Iterator を返す | |
* (1, 2, 3) -> ([1, 2], [2, 3], [3, undefined]) | |
* | |
* @param iterable | |
*/ | |
function* withNext<T>(iterable: Iterable<T>): IterableIterator<[T, T | undefined]> { | |
let isStart = true; | |
let prev: T | null = null; | |
for (const current of iterable) { | |
if (isStart) { | |
isStart = false; | |
} else { | |
yield [prev!, current]; | |
} | |
prev = current; | |
} | |
if (!isStart) { | |
yield [prev!, undefined]; | |
} | |
} | |
interface ResponsiveConfig { | |
[key: string]: number; | |
} | |
interface ResponsiveBreakpoint<T extends ResponsiveConfig> { | |
key: keyof T; | |
min: number; | |
max: number | null; | |
} | |
/** | |
* レスポンシブデザイン対応のための React Custom Hooks | |
* | |
* @example | |
* const responsive = useResponsive({ | |
* xs: 0, | |
* sm: 480, | |
* md: 1024, | |
* }); | |
* | |
* @param config | |
*/ | |
export function useResponsive<T extends ResponsiveConfig>(config: T): keyof T { | |
const breakpoints: readonly ResponsiveBreakpoint<T>[] = useMemo(() => { | |
const sorted = Object.entries(config) | |
.map(([key, min]) => ({ key, min })) | |
.sort(({ min: min1 }, { min: min2 }) => { | |
return min1 - min2; | |
}); | |
return [...withNext(sorted)].map(([{ key, min }, nextBreakpoint]) => { | |
let max = null; | |
if (nextBreakpoint !== undefined) { | |
max = nextBreakpoint.min - 0.01; | |
} | |
return { key, min, max }; | |
}); | |
}, [config]); | |
const [responsive, setResponsive] = useState<keyof T>(breakpoints[0].key); | |
useEffect(() => { | |
const queries: MediaQueryList[] = []; | |
const queryKeys: WeakMap<MediaQueryList, keyof T> = new WeakMap(); | |
for (const { key, min, max } of breakpoints) { | |
let raw = "screen"; | |
if (min !== 0) { | |
raw += ` and (min-width: ${min}px)`; | |
} | |
if (max !== null) { | |
raw += ` and (max-width: ${max}px)`; | |
} | |
// eslint-disable-next-line no-undef | |
const query = matchMedia(raw); | |
if (query.matches) { | |
setResponsive(key); | |
} | |
queries.push(query); | |
queryKeys.set(query, key); | |
} | |
const handleChange = function (this: MediaQueryList, e: MediaQueryListEvent): void { | |
if (!e.matches) { | |
return; | |
} | |
setResponsive(queryKeys.get(this)!); | |
}; | |
for (const query of queries) { | |
// eslint-disable-next-line no-undef | |
if (query instanceof EventTarget) { | |
query.addEventListener("change", handleChange); | |
} else { | |
(query as MediaQueryList).addListener(handleChange); | |
} | |
} | |
return (): void => { | |
for (const query of queries) { | |
// eslint-disable-next-line no-undef | |
if (query instanceof EventTarget) { | |
query.removeEventListener("change", handleChange); | |
} else { | |
(query as MediaQueryList).removeListener(handleChange); | |
} | |
} | |
}; | |
}, [breakpoints]); | |
return responsive; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment