5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザのウィンドウサイズの変更を検知するカスタムフックを2種類試してみました

Last updated at Posted at 2024-09-19

はじめに

こんにちは、Gakken LEAPのフロントエンドエンジニアの Okuma です。

引き続きフロントエンドエンジニアとして、UI・UXを通してサービスの改善を進めております。
前回は少しハンズオン形式でReactのライブラリであるFramer Motionを使ってUIにアニメーションを適用させる方法について紹介させていただきました。

今回はNext.jsでWebサイトを作成している際にウィンドウサイズの変更検知について色々と試行錯誤したので、そのことについて書いてみようと思います。

背景

Next.js x TailwindCSSの構成でサイトを作成していたので、レスポンシブ対応はTailwindCSSに定義されているブレイクポイントを使用してスマホなどの小さいデバイスに対応するレイアウトを作成していました。
スタイルシートで定義している箇所はもちろんこの対応のみであらゆるデバイスに対応できると思いますが、今回は前回の記事でもご紹介したFramer Motionを使用してアニメーションも加えていたため、デバイスサイズによってアニメーションのプロパティ、つまりアニメーションの表現方法を変えたいという思いがありました。(前回の記事はこちら)
ただ、CSSはTailwindのブレイクポイントを使用すれば良いですが、Framer Motionのプロパティの値はウィンドウサイズが変わった際にどのように変更させればよいのかということで壁に当たりました。
今回のその解決のためにuseWindowResizeuseMediaQueryという2つのカスタムフックを作成してみたので、それぞれご紹介していきます。

useWindowResize

最初に作成したuseWindowResizeでは、window.addEventListener('resize')を使用してウィンドウサイズを取得します。順を追って作成していきましょう。

import { useState } from "react";

interface WidthAndHeight {
  width: number;
  height: number;
}

const useWindowResize = () => {
  const [widthAndHeight, setWidthAndHeight] = useState<WidthAndHeight>({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  return widthAndHeight;
};

export default useWindowResize;

まずはじめにuseStateでカスタムフックから返すwidthheightのオブジェクトを定義します。初期値はwindow.innerWidthwindow.innerHeightにして、ページを開いた際のウィンドウサイズを取得するようにしています。
これでこのフックを利用するとその時のウィンドウサイズを取得できるようになります。ここからウィンドウサイズが変化した際にこの値をアップデートして常に最新のウィンドウサイズを取得できるようにしてみましょう。

// ↑省略
const useWindowResize = () => {
  const [widthAndHeight, setWidthAndHeight] = useState<WidthAndHeight>({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  const handleResize = () => {
    setWidthAndHeight({
      width: window.innerWidth,
      height: window.innerHeight,
    });
  };

  return widthAndHeight;
};

export default useWindowResize;

handleResizeを追加しました。これはウィンドウサイズが変更したのを検知した際に実行するイベントハンドラになります。最初に定義したstateに現在のinnerWidthinnerHeightを代入します。

// ↑省略
const useWindowResize = () => {
  const [widthAndHeight, setWidthAndHeight] = useState<WidthAndHeight>({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  const handleResize = () => {
    setWidthAndHeight({
      width: window.innerWidth,
      height: window.innerHeight,
    });
  };

  useEffect(() => {
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return widthAndHeight;
};

export default useWindowResize;

最後にuseEffectを追加してウィンドウサイズの変更を検知してイベントハンドラを実行するようにします。
useEffectは、コンポーネントがレンダリングされた後に副作用を実行するために使われます。ここでは、ウィンドウのリサイズイベントを監視し、対応する処理を行うために使っています。
また、return文でクリーンアップ関数を定義しています。これは、コンポーネントがアンマウントされたときに呼び出され、リサイズイベントの監視を解除します。これによって不要なイベントリスナーが残ってメモリリークを引き起こすことを防ぐことができます。

あとはこのuseWindowResizeをページなどで呼び出して使用すると、現在のウィンドウサイズが取得できます。
実際に触ってみたい方のためにCodesandboxも貼っておきます。

useMediaQuery

続いてuseMediaQueryを作成していきましょう。
こちらはwindow.matchMediaを使ってカスタムフックを作成します。
window.matchMediaはドキュメントによると、「指定された メディアクエリ文字列のパース結果を表す、新しい MediaQueryList オブジェクトを返します。」となっています。

import { useState } from "react";

const useMediaQuery = (query: string) => {
  const [matches, setMatches] = useState<boolean>(false);

  return matches;
};

export default useMediaQuery;

まずはuseMediaQueryの大枠を定義していきます。window.matchMediaではメディアクエリ文字列を引数として渡すことになるので、フックの引数(query)として受け取るようにします。
最終的に戻り値となるのはbooleanなので、useStateで保持しているmatchesを返してあげます。

import { useEffect, useState } from "react";

const useMediaQuery = (query: string) => {
  const [matches, setMatches] = useState<boolean>(false);

  useEffect(() => {
    const mediaQuery: MediaQueryList = window.matchMedia(query);
    const handleMediqQueryChange = (event: MediaQueryListEvent) => {
      setMatches(event.matches);
    };

    setMatches(mediaQuery.matches);
  }, []);

  return matches;
};

export default useMediaQuery;

useEffect内でwindowオブジェクトを使って実際にwindow.matchmediaを呼び出し、その引数としてメディアクエリ文字列を渡してあげます。ここでの戻り値はMediaQueryListという型で帰ってくるので、この中の.matchesにアクセスすることでbooleanの値を取得できので、これをstateの値として保持します。

また、handleMediaQueryChangeはイベントリスナーとして使用します。

import { useEffect, useState } from "react";

const useMediaQuery = (query: string) => {
  const [matches, setMatches] = useState<boolean>(false);

  useEffect(() => {
    const mediaQuery: MediaQueryList = window.matchMedia(query);
    const handleMediqQueryChange = (event: MediaQueryListEvent) => {
      setMatches(event.matches);
    };

    mediaQuery.addEventListener("change", handleMediqQueryChange);
    setMatches(mediaQuery.matches);

    return () => {
      mediaQuery.removeEventListener("change", handleMediqQueryChange);
    };
  }, [query]);

  return matches;
};

export default useMediaQuery;

最後にuseEffectの副作用として実行するaddEventListenerとクリーンアップ関数であるremoveEventListenerを記述します。

ただ、今回は第一引数であるイベントタイプをchangeとしているので、具体的に以下のタイミングで実行されます。

  1. ウィンドウサイズの変更など、画面の仕様や設定が変化したタイミング
  2. ユーザーが画面の向き(横/縦)を変更したタイミング
  3. ユーザーがデバイスの設定を変更し、指定したメディアクエリが一致/不一致になったタイミング

ウィンドウサイズそのものが変化したタイミングでresizeイベントが発生する一方で、changeイベントは指定したメディアクエリの一致状態が変化したタイミングで発生するという明確なタイミングがあります。

useWindowResizeuseMediaQueryではどちらもウィンドウサイズの変化に対応するために使用するカスタムフックですが、useWindowResizeは単純にウィンドウサイズの変更のみを検知し、useMediaQueryは指定したメディアクエリの一致状態の変化を検知するカスタムフックになっているため、状況に応じて使い分けることができるのです。

使用する際の注意点

今回はブラウザ上で動くことを想定しているため、windowオブジェクトを使用しています。サーバーサイドレンダリングやサーバーコンポーネントを使用する際には注意が必要です。
また、スタイリングでのレスポンシブ対応で使用する場合はまず最優先としてCSSのメディアクエリを使用するようにしましょう。(今回はTailwindCSSのメディアクエリ)
スタイリングでどうしようもならない場合にのみ今回のカスタムフックを適宜使用するイメージです!

まとめ

もともと私はuseWindowResizeを使用していましたが、TailwindCSSのブレイクポイントに合わせてFramer motionのアニメーションを変化させたかったので、この用途ではブレイクポイントを超えたかどうかだけを判断してくれるuseMediaQueryの方が使い勝手が良さそうでした。

エンジニア募集中

Gakken LEAP では教育をアップデートしていきたいエンジニアを絶賛大募集しています!!
ぜひお気軽にカジュアル面談へお越しください!!

参考

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?