かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

TypeScript レスポンシブ時のイベントには MediaQueryListAPI matchMedia が便利だった!

レスポンシブ時の View の変化は基本的に CSS のメディアクエリで行いたいが、 window サイズが大きくなったときにモバイル用のモーダルメニューを閉じるとか、意外とレスポンシブ時に JS でイベントを実行したい時がある

window.resize を使った今までやりかた

const breakpoint = 768 as const;

export const App = () => {
  const timerRef = useRef<number>(0);
  const [isSp, setIsSp] = useState<boolean>(true);

  useEffect(() => {
    const timer = timerRef.current;
    const resizeEvent = () => {
      window.clearTimeout(timer);
      window.setTimeout(() => {
        if (window.innerWidth >= breakpoint) {
          setIsSp(false);
        } else {
          setIsSp(true);
        }
      }, 100);
    };

    // レンダリング時に window サイズをチェックするために呼び出す
    resizeEvent();
    // add addEventListener
    window.addEventListener("resize", resizeEvent);
  
    return () => {
      window.clearTimeout(timer);
      window.removeEventListener("resize", resizeEvent);
    };
  }, []);
  
  return (<div>{isSp? "true" : "false"}</div>)
}

今までは window.resize イベントを監視して特定の window サイズになった際にイベントを発火させていたがイベントを間引く debounde の処理などいろいろと面倒な部分があった
また、debounce させていても window がリサイズされる事にイベントが呼び出されるので処理が多くなってしまう

MediaQueryListAPI matchMedia を使った方法

const breakpoint = 768 as const;

export const App = () => {
  const [isSp, setIsSp] = useState<boolean>(true);

  useEffect(() => {
    const mediaQueryList = window.matchMedia(`(min-width: ${breakpoint}px)`);
    const handleOrientationChange = (
      evt: MediaQueryList | MediaQueryListEvent
    ) => {
      if (evt.matches) {
        setIsSp(false);
      } else {
        setIsSp(true);
      }
    };

    // レンダリング時に window サイズをチェックするために呼び出す
    handleOrientationChange(mediaQueryList);
    // add addEventListener
    mediaQueryList.addEventListener("change", handleOrientationChange);

    return () => {
      mediaQueryList.removeEventListener("change", handleOrientationChange);
    };
  }, []);
  
  return (<div>{isSp? "true" : "false"}</div>)
}

MediaQueryListAPI を使った方法だと設定したブレイクポイントを跨いだタイミングでだけ change イベントが発火される
リスナー関数の引数は初回呼び出し時は MediaQueryList で change イベントから受け取る場合は MediaQueryListEvent になるようなので evt: MediaQueryList | MediaQueryListEvent とした。もっと良い指定方法があるかもしれない。

Sample

次のサンプルでは breakpoint を跨ぐと各エリアのデザインが変更され、発火されたイベントの回数を表示している。
matchMedia を使った方は必要なときだけイベントが発火しているのが分かると思う


[参考]

ウェス・アンダーソンもともと好きだったけど、グランド・ブダペスト・ホテル は本当に良かった!