メディアクエリを扱うReact Hooksのカスタムフックを作った話
React Hooksでぼちぼちコードを書いているのだけれども、今日カスタムフックの実装をしてようやくReact Hooksの勘所が少し理解できたような気がするので、忘れないうちにメモ代わりに書いています。
今実装しているコードで、画面幅に合わせて異なるReact Componentを返したいケースがあって、最初は画面幅をwindow.innerWidth
で取得して場合分けするコードをcomponentDidMount
に記述して実現していましたが、ロード後に画面サイズを変更した場合にそれが反映されない問題がありました。
基本的にはデバイス毎に異なるコンポーネントをrenderしたり、propsの値を変えたいだけなので、それほど大きな問題にはなっていませんが、
出来ればなんとかして動的な画面サイズの変更に合わせて切り替えられるようにしたいなあと思っていました。
その実現のためにuseMediaQuery
というカスタムフックを実装してみました。
引数には区別したいメディアクエリの配列を渡し、返り値にその中で現在のメディアクエリを返すようになっています。
画面幅の変更等でマッチするメディアクエリが変わった場合にはuseMediaQuery
の返す値が変わり、Reactコンポーネントが更新されるようになっています。
割とシンプルなコードで実現できました。
以下がuseMediaQuery
の実装です。
import { useEffect, useState } from 'react' const getCurrentMedia = (mediaList) => { let result = null; for (const media of mediaList) { if (window.matchMedia(media).matches) { result = media break } } return result } const useMediaQuery = (mediaList) => { const [current, setCurrent] = useState(getCurrentMedia(mediaList)) useEffect(() => { let mounted = true let timeout const onResize = () => { // 500msに一度しかresizeイベントのcallbackを実行しないようにする if (timeout) return const media = getCurrentMedia(mediaList) if (current !== media) { setCurrent(media) } timeout = setTimeout(() => timeout = null, 500) } window.addEventListener('resize', onResize) return () => { mounted = false window.removeEventListener('resize', onResize) } }, []) return current } export default useMediaQuery
実装はreact-useのuseOrientation
を参考にしました。
ReactのVirtualDOMの外の世界であるwindow.innerWidth
やresizeイベントをカスタムフック内に隠蔽し、
useStateを利用してその結果だけをReactの世界で管理するようになっています。
これにより、実装時にはresizeイベントのことを気にせず、文字列としてメディアクエリを取得することが出来るようになります。
利用する際は以下のように表示を切り分けたいメディアクエリの配列を渡すことで、現在の状態にマッチするものが返されるので、その値を利用して描画処理を行います。
import { useMemo } from 'react' import 'useMediaQuery' from './useMediaQuery' const QUERY_SP = "(max-width: 640px)" const QUERY_PC = "(min-width: 641px)" const Comp = (props) => { // queryには現在の画面幅に応じて、QUERY_SPもしくはQUERY_PCの文字列が返される const query = useMediaQuery([QUERY_SP, QUERY_PC]) const text = useMemo(() => { switch(query) { case QUERY_SP: return "スマートフォン" case QUERY_PC: return "PC" } }, [query]) return <span>現在は{text}で表示しています。</span> } export default Comp
こんな感じで、実装している側ではresizeイベントを全く考慮すること無く実装することが出来ました。
結構個人的には革命的だったというか、Reactでどうしても実装が綺麗に出来ないと思ってたところがとてもすっきりしたので、 仕事中にもかかわらず思わず興奮してしまいました。
そのうちuseMediaQueryはテストコード書いたり、言語をTypeScriptで書き直した上でGitHub上で公開しようかなとは思っています。 カスタムフックのライブラリってどういう粒度で公開したらいいのかちょっと悩ましいですね。 フック1つでライブラリ1つにすると、なんか大量のライブラリを管理しなきゃいけなさそうで面倒だなあとは思いつつ、 react-useみたいにユーティリティライブラリみたいにする場合、他にどういうカスタムフックとセットにすべきかというのもなかなか悩ましいところ。