🎨

React のレスポンシブ実装おすすめ 2023年版

2023/05/05に公開1

みなさまにおかれましてはプロダクト開発でレスポンシブ対応はしていますでしょうか?

自分はここ数年 PC 向け画面の開発ばかりやっており、今年になって久しぶりにレスポンシブ対応をがっつりやったのですが、最近の React 環境下でのレスポンシブ対応はどんな形でやると良い感じに使いやすくなるかなあと、社内のメンバーに相談しつつ調査しつつ、結果としてわりと納得できる形になったので、せっかくなのでここでシェアさせていただこうかなと思いこの記事を書くに至ります。

PC とスマホで、CSS が変わる程度のレスポンシブであれば CSS で media query を書けば済むので問題はないのですが、たまに HTML 構造ががっつり変わるのでコンポーネントレベルの出し分けが必要になるときがあります。
今回はそのようなケースを対象としています。

おすすめのレスポンシブ実装

早速ですがとりあえずコードを貼ります。

import { useEffect, useState } from 'react'

export const mediaQuery = {
  sp: 'width < 752px',
  tablet: '752px <= width < 1122px',
  pc: '1122px <= width',
}

export const useMediaQuery = (query: string) => {
  const formattedQuery = `(${query})`
  const [match, setMatch] = useState(matchMedia(formattedQuery).matches)

  useEffect(() => {
    const mql = matchMedia(formattedQuery)

    if (mql.media === 'not all' || mql.media === 'invalid') {
      console.error(`useMediaQuery Error: Invalid media query`)
    }

    mql.onchange = (e) => {
      setMatch(e.matches)
    }

    return () => {
      mql.onchange = null
    }
  }, [formattedQuery, setMatch])

  return match
}

使う側は以下のように使用します。

import { mediaQuery, useMediaQuery } from './useMediaQuery'
import { SpComponent } from './SpComponent'
import { PcComponent } from './PcComponent'

export const Component = () => {
  const isSp = useMediaQuery(mediaQuery.sp)

  if (isSp) {
    return <SpComponent />
  }

  return <PcComponent />
}

簡単ですね。

解説

ポイントとしては、以下の3点。

  • hooks で実装する
  • matchMedia を使う
  • media query は range syntax を使う

hooks で実装する

まずインターフェースとしては hooks の形で実装しています。
コンポーネントの形で提供してスマホサイズのときのコンポーネントや PC サイズのときのコンポーネントを props として渡してもらうとかも考えたのですが、より汎用性を上げてるために hooks で実装して boolean を返すインターフェースにしています。

matchMedia を使う

const mql = matchMedia(formattedQuery)

横幅の判定方法として、matchMedia を使用しています。
window.matchMedia - Web API | MDN

CSS の media query の文字列を渡して MediaQueryList を返してくれる関数です。

MediaQueryList には、単純に現在の横幅が条件に一致しているかの matches (boolean) や、window が resize される度に発火する onchange というハンドラーが含まれます。
onchange に関しては、ハンドラーが含まれるというより、MediaQueryList の onchange に関数を渡すことで window の resize の度にそれが発火されるという感じです。
(onchange ではなく addEventListener, removeEventListener を使うこともできます)

昔は innerWidth を取得して判定したり、useEffect の中で addEventListener で resize を見ていたりしてましたが、matchMedia を使うことでだいぶシンプルに書けるようになりました。

ブラウザ互換性も問題ありません。
"matchmedia" | Can I use... Support tables for HTML5, CSS3, etc

media query は range syntax を使う

export const mediaQuery = {
  sp: 'width < 752px',
  tablet: '752px <= width < 1122px',
  pc: '1122px <= width',
}

そして matchMedia に渡す media query は range syntax を使っています。

昔はタブレットサイズは横幅が752pxから1122pxの間というのを表現するには @media (min-width: 753px) and (max-width: 1121px) という感じで結構記述がめんどくさかったのですが、それが比較演算子を用いることでシンプルに書けるようになりました。

こちらもほぼほぼ全てのブラウザで問題なく使用できます。
"media query range syntax" | Can I use... Support tables for HTML5, CSS3, etc

その他補足

インターフェースとして、デフォルトでは sp, tablet, pc という3種類の定数を用意していますが、ごく稀にそこに当てはまらず任意の横幅でスタイルを変えたいときがやってくることがあります。
そのときのために、任意の query を渡すことができるようになっていて、その場合は const result = useMediaQuery('width < 800px') という形になります。

おわり

実装としてはとてもシンプルな形になったし、コードも読みやすく、また使用する側もわかりやすい形になりました。
media query 周りの仕様も最近はとても使いやすい API がほとんどのブラウザで使えるようになっており、本当にありがたい限りです。

GitHubで編集を提案

Discussion

kankan

ありがとうございます。
一点補足です。

iPhoneでブレイクポイントが作動しなかったのですが、max-widthを使用することで作動するようになりました。
osをアプデしてない人は結構いるかと思いますので、当面は以下が安全かもしれません。

export const mediaQuery = {
  sp: "max-width: 752px",
  tablet: "min-width: 752px and max-width: 1122px",
  pc: "min-width: 1122px",
};