😺

js版メディアクエリをmatchMediaを用いて実装

2022/01/17に公開

こんにちは。人材系事業会社でフロントエンドエンジニアをしている深澤です。
今日は、js版メディアクエリhooksを紹介したいと思います。

Reactでレスポンシブなサイトを実装する際にPC版とSP版で表示したい要素が変わるといった、CSSのメディアクエリではカバーしきれない要件の画面があると思います。
CSSのメディアクエリでdisplay: none;することもできますが、DOM要素としては残ってしまいますし、あまりイケてる実装ではないですよね。
そういったときにjsxに式を埋め込む形で表示制御を行うことのできる実装になっています。
window関数を用いているため、その他のフロントエンドライブラリを使用していても使用可能な実装となっています。のでAngularやVueを使っている方も参考にしていただければと思います。

window.matchMedia

今回紹介するhooksはwindow.matchMedia を使用しています。
matchMediaはCSSのメディアクエリ文字列を渡すことで、そのパース結果を返すwindow関数です。
現在はどのブラウザでも使用可能です。
ref) https://caniuse.com/matchmedia

従来画面サイズをリサイズしたときに処理を走らせたい場合は以下のようにイベントリスナーを設定していたかと思います。

window.addEventListener('resize', () => {
  if (window.innerWidth <= SOME_WINDOW_WIDTH) {
    // 何らかの処理
  }
})

上記のデメリットは1px画面サイズが変化するごとにイベントが発火するため、重い処理を行うときなどオーバーヘッドが出やすいです。
特にIEなどのブラウザではもっさりとした動作になってしまうこともあります。

matchMediaの利点は設定したピクセル数を超えた場合のみイベントが発火するという点です。
画面サイズによって要素の出し分けを行いたい場合では、addEventListener('resize', () => {})のように1pxの変化ごとにイベント発火する必要もなく、特定のピクセル数を超えた場合に表示判定ができればよいため、適した実装と言えるでしょう。

useMediaQuery

実際のソースコードを以下に添付します。

import {useCallback, useEffect, useState} from 'react'

type WidthPrefix = 'min' | 'max'

const createQuery = (width: number, prefix: WidthPrefix) =>
  `(${prefix}-width: ${width}px)`

export const useMediaQuery = (width: number, prefix: WidthPrefix = 'min') => {
  const query = createQuery(width, prefix)
  const [matchQuery, setMatchQuery] = useState(matchMedia(query))

  const handleQueryListener = useCallback(() => setMatchQuery(matchMedia(query)), [query])

  useEffect(
    () => {
      matchQuery?.addEventListener('change', handleQueryListener)
      return () => matchQuery?.removeEventListener('change', handleQueryListener)
    },
    [handleQueryListener, query]
  )

  return matchQuery.matches
}

コード解説

useMediaQueryに渡す引数は2つです。

1つめは閾値となるwidthです。
2つめは閾値以上(min)か閾値以下(max)かを指定します。
第2引数については固定値でも良いと思います。ユースケースによって最適化してください。

matchMediaに渡す引数はCSSのメディアクエリと同様にmin-width: 966pxといった文字列を渡します。
matchQueryの型は MediaQueryList という型になっています。
matchQuery.matchesはメディアクエリリストにマッチする場合にtrueを返します。

useEffectの処理はメディアクエリのマッチ状況が変化した場合にイベントリスナーが発火されるようにコールバックをセットしておきます。
コールバックはクエリ文字列が変化しない限り同じ結果を返すのでuseCallbackでキャッシュしておきます。
また、useEffectのクリーンアップ関数でremoveEventListenerもセットしておきます。

使用方法

import React, {FC} from 'react'
import {useMediaQuery} from '../useMediaQuery'

const LAPTOP_WIDTH = 966

export const HogeComponent: FC = () => {
  const isLaptop = useMediaQuery(LAPTOP_WIDTH, 'min')
  
  return <>
    {isLaptop && <>966px以上で表示されるよ</>}
    {!isLaptop && <>965px以下で表示されるよ</>}
  </>
}

上記のように閾値と閾値以上か以下かを引数で渡すと、宣言的にboolean値が返却されます。
あとはjsx内で表示制御を行えば完璧です。

閾値となる値は定数化して、CSSと共通化できるとよいですね。

さいごに

ざっくりとな説明になりましたがいかがだったでしょうか。
各プロダクトで使用箇所のルールを決めていただいて運用していただければ、レスポンシブサイトでかなり有益なhooksかと思います。
(CSSでカバーできる範囲はCSSでメディアクエリ書くなど)

ここまで説明しておいてアレなんですが、できればこのhooksが使われないデザインになっているほうが良いと思いますので、デザイナーの方もし読んでいただけたらデザインの再考のほどよろしくお願いします :)

GitHubで編集を提案

Discussion