雑に Responsive Design 用の React Custom Hooks を作ったやつ。
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 - 1; | |
} | |
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