はじめに
こんにちは、Gakken LEAPのフロントエンドエンジニアの Okuma です。
引き続きフロントエンドエンジニアとして、UI・UXを通してサービスの改善を進めております。
前回は少しハンズオン形式でReactのライブラリであるFramer Motionを使ってUIにアニメーションを適用させる方法について紹介させていただきました。
今回はNext.jsでWebサイトを作成している際にウィンドウサイズの変更検知について色々と試行錯誤したので、そのことについて書いてみようと思います。
背景
Next.js x TailwindCSSの構成でサイトを作成していたので、レスポンシブ対応はTailwindCSSに定義されているブレイクポイントを使用してスマホなどの小さいデバイスに対応するレイアウトを作成していました。
スタイルシートで定義している箇所はもちろんこの対応のみであらゆるデバイスに対応できると思いますが、今回は前回の記事でもご紹介したFramer Motionを使用してアニメーションも加えていたため、デバイスサイズによってアニメーションのプロパティ、つまりアニメーションの表現方法を変えたいという思いがありました。(前回の記事はこちら)
ただ、CSSはTailwindのブレイクポイントを使用すれば良いですが、Framer Motionのプロパティの値はウィンドウサイズが変わった際にどのように変更させればよいのかということで壁に当たりました。
今回のその解決のためにuseWindowResize
とuseMediaQuery
という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
でカスタムフックから返すwidth
とheight
のオブジェクトを定義します。初期値はwindow.innerWidth
とwindow.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
に現在のinnerWidth
とinnerHeight
を代入します。
// ↑省略
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
としているので、具体的に以下のタイミングで実行されます。
- ウィンドウサイズの変更など、画面の仕様や設定が変化したタイミング
- ユーザーが画面の向き(横/縦)を変更したタイミング
- ユーザーがデバイスの設定を変更し、指定したメディアクエリが一致/不一致になったタイミング
ウィンドウサイズそのものが変化したタイミングでresize
イベントが発生する一方で、change
イベントは指定したメディアクエリの一致状態が変化したタイミングで発生するという明確なタイミングがあります。
useWindowResize
とuseMediaQuery
ではどちらもウィンドウサイズの変化に対応するために使用するカスタムフックですが、useWindowResize
は単純にウィンドウサイズの変更のみを検知し、useMediaQuery
は指定したメディアクエリの一致状態の変化を検知するカスタムフックになっているため、状況に応じて使い分けることができるのです。
使用する際の注意点
今回はブラウザ上で動くことを想定しているため、window
オブジェクトを使用しています。サーバーサイドレンダリングやサーバーコンポーネントを使用する際には注意が必要です。
また、スタイリングでのレスポンシブ対応で使用する場合はまず最優先としてCSSのメディアクエリを使用するようにしましょう。(今回はTailwindCSSのメディアクエリ)
スタイリングでどうしようもならない場合にのみ今回のカスタムフックを適宜使用するイメージです!
まとめ
もともと私はuseWindowResize
を使用していましたが、TailwindCSSのブレイクポイントに合わせてFramer motionのアニメーションを変化させたかったので、この用途ではブレイクポイントを超えたかどうかだけを判断してくれるuseMediaQuery
の方が使い勝手が良さそうでした。
エンジニア募集中
Gakken LEAP では教育をアップデートしていきたいエンジニアを絶賛大募集しています!!
ぜひお気軽にカジュアル面談へお越しください!!