はじめに
React18以降に、useSyncExternalStoreというフックが導入されました(公式ドキュメント)。このフックは、外部ストア(ブラウザAPIやサードパーティの状態など)とReactの状態を同期させるためのものだそうです。今回は、これを使ってメディアクエリを扱うカスタムフックを作ってみました。実際に使ってみた感想やコードを共有します!
使ってみた
以下が、useSyncExternalStoreを使ったuseMediaQueryフックのコードです。ウィンドウサイズに応じてメディアクエリを監視し、結果をbooleanで返します。
import { useSyncExternalStore } from "react";
const subscribe = (query: string) => (callback: () => void) => {
const mediaQueryList = window.matchMedia(query)
mediaQueryList.addEventListener("change", callback)
return () => mediaQueryList.removeEventListener("change", callback)
};
const getSnapshot = (query: string) => () => {
if (typeof window === "undefined") {
return false // サーバーサイドではfalseを返す
}
return window.matchMedia(query).matches;
};
/**
* コンポーネント表示
*
* ```tsx
* // in component
* const isMobile = useMediaQuery('(max-width: 767px)')
* return <div>{isMobile ? 'モバイル' : 'デスクトップ'}</div>;
* ```
*/
export const useMediaQuery = (query: string): boolean => {
return useSyncExternalStore(subscribe(query), getSnapshot(query));
};
ウィンドウサイズが変わると、useMediaQueryが自動的に再評価され、UIが更新されます。
subscribeとgetSnapshotについて
useSyncExternalStoreを使う上で、2つの関数subscribeとgetSnapshotの役割を理解するのが大事です。それぞれの仕事を整理してみました。
subscribeの役割
- 外部ストア(ここでは
window.matchMedia)の変更を監視し、変化があったときにReactに通知します。
getSnapshotの役割
- 外部ストアの現在の状態を取得して返します。
subscribeが変更を検知したときに呼ばれます。今のウィンドウサイズがクエリに該当するかをbooleanで返します - ポイント: 「今どうなってるか」を答える役割で、監視はしません。
役割分担の意味
- 責務の分離: subscribeが「タイミング」を、getSnapshotが「値」を担当することで、コードがシンプルに
- 効率性: 変更があったときだけgetSnapshotが呼ばれ、値が変わった場合にのみ再レンダリング
- 柔軟性: 外部ストアが何であれ、同じパターンで対応可能
簡単に言うと、subscribeは「変化をキャッチするセンサー」、getSnapshotは「現在の値をチェックするメーター」です。この2つが協力して、外部の状態とReactの表示を同期させています。
感想
-
良かった点:
useSyncExternalStoreを使うと、従来のuseEffect+addEventListenerの手動管理が不要になり、コードがスッキリした。SSR(サーバーサイドレンダリング)対応も簡単に考慮できるのが嬉しい -
気をつける点:
subscribeとgetSnapshotの役割分担を理解するのが難しかった。公式ドキュメントをじっくり読むと腑に落ちた
まとめ
useSyncExternalStoreは、Reactと外部の状態を同期させるのにぴったりのフックだと感じました。メディアクエリ以外にも、WebSocketやブラウザのAPIを使う場面で活躍しそうです。みなさんもぜひ試してみてください!