【React】useMediaQuery は最終手段にしよう
こんにちは、エンジニアです。
本記事ではuseMediaQuery
を使うべきではない理由を説明します。
useMediaQuery
とは
window.matchMedia
の判定結果を取得するカスタムフックを指します。
const isWideScreen = useMediaQuery("(min-width: 768px)");
window.matchMedia
はCSSでできるメディアクエリの判定をJavaScriptでも可能にするブラウザAPIです。それをReact Hooksと組み合わせることで、宣言的に判定を行えるようなカスタムフックとなります。
過去にuseSyncExternalStore
を使って実装する記事を書いたので参考にしてみてください。
上の記事ではuseSyncExternalStore
の使い道を説明するのが目的だったのですが、Reactのフックとしてメディアクエリを使える、使っても良いと捉えられてしまったようで、題材選びを少し反省しています(?)
以下はユーティリティ系ライブラリによる提供例です。関数名は提供元によってバラツキがありますが、おおよそ同じことをしています。
- react-responsive - useMediaQuery
- react-use - useMedia
- usehooks-ts - useMediaQuery
- @uidotdev/usehooks - useMediaQuery
- その他色々
問題点
useMediaQuery
はwindow
オブジェクトに依存しています。つまりブラウザ上でしか正しく判定できません。
一方で昨今はサーバーサイドレンダリング(SSR)でHTMLを生成してからブラウザ上でイベントハンドラーを復元するhydrationを行います。サーバーサイドにはwindow
オブジェクトがないため、固定値を返すかエラーを投げるしかありません。
サーバーサイドで固定値を返す場合、その固定値と実際のブラウザ上で計算される値が異なる可能性があります。作りが悪いカスタムフックだと、これがhydrationエラーとしてレポートされることになります。作りが良くて(?)useSyncExternalStore
やuseEffect
を上手く駆使しているとしても、それをスタイリングの制御に使っている場合はレイアウトシフトとして悪影響を及ぼします(本記事では適用されるCSSが突然変化してチラチラして見える現象を総称してレイアウトシフトと呼ぶことにします)。
レイアウトシフトになるのは、以下の順序で処理が行われるためです。
- サーバーサイドで固定値を使ってHTMLが生成される
- ブラウザが固定値を使って生成されたHTMLを画面に反映する
- 画面反映後
useEffect
でmatchMedia
の計算を実行する -
matchMedia
の計算結果を改めて画面に反映する
敢えてuseEffect
と書いたのはuseSyncExternalStore
を使ってもuseEffect
と変わらないと言えるためです。それは React17 以前のバージョン向けに用意されたuseSyncExternalStore
のポリフィルから判断できます。ポリフィルのソースコードを見ると、useSyncExternalStore
はuseEffect
(またはuseLayoutEffect
)で代替実装ができるフックです。
解決策
スタイリングの制御に使うなら、まずCSSのメディアクエリで実装できないか検討してください。CSSで実装すれば最初からブラウザのサイズが考慮されたスタイリングが当たり、レイアウトシフトの原因にはなりません。
スタイリングではない箇所、例えばイベントハンドラーやuseEffect
で画面幅を取りたい場合は、それらの関数の中で計算すればよいです。カスタムフックは必要ありません。
使っても良い箇所
SSR対応フレームワークを使用していない(ブラウザで空のHTMLにSPAをマウントして使っている)のであれば、どんな箇所でも使用可能です。
SSR対応フレームワークでも、クライアントサイドレンダリングだけで使用されるコンポーネントなら使っても悪い現象は起きません。ユーザーインタラクションによって初めて表示されるダイアログなどが該当します。ちなみに、「クライアントサイドレンダリングだけ」というのは"use client"
を付けることではありません。
dynamic importされるコンポーネントでも良いです。dynamic importされたコンポーネントはクライアントサイドレンダリングであることが保証されます。Next.js ならnext/dynamic
も含みます。
@uidotdev/usehooksにはuseIsClient
というカスタムフックがあり、その戻り値でクライアントサイドかを判定してコンポーネントを表示するかどうか決めることもできます。
色々回避策はありますが、スタイリングを切り替えたいだけならやはりCSSだけで実装できないか検討してください。スタイリングはCSSの役割です。
@uidotdev/usehooksの方針
@uidotdev/usehooksはブラウザJavaScriptのAPIを使用するカスタムフックがサーバーサイドレンダリング中に実行される場合、エラーをthrowする方針のようです。
「hydrationエラーをごまかすためだけの作り方は好きではない」という強い意思を持ってエラーとしているようで、僕はとても好きな方針です。
まとめ
-
useMediaQuery
は使わずに、CSSのメディアクエリで実現できないかを検討する - 仮に使うことになっても、必ずユーザーに違和感を与えるチラツキを抑える
- ライブラリがSSRでエラーをthrowするのは思想なので従おう
- 回避策はあるけど回避するな
それでは良いReactライフを!
ちょっと株式会社(chot-inc.com)のエンジニアブログです。 フロントエンドエンジニア募集中! カジュアル面接申し込みはこちらから chot-inc.com/recruit/iuj62owig
Discussion