Skip to content

Instantly share code, notes, and snippets.

@petamoriken
Last active October 11, 2022 03:41
Show Gist options
  • Save petamoriken/070878457954f05a1f54e8491b9b1fd9 to your computer and use it in GitHub Desktop.
Save petamoriken/070878457954f05a1f54e8491b9b1fd9 to your computer and use it in GitHub Desktop.
雑に 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 - 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