はじめに
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を使う場面で活躍しそうです。みなさんもぜひ試してみてください!