初めてのuseSyncExternalStore
はじめに
皆さんは、useSyncExternalStore
というフックを使ったことはありますか?
React 18 から導入されたフックで、ストア関連(状態管理ライブラリ)周りで知ったのですが、このフックは、それ以外の用途でも使うことができるみたいで、今回はそのことについてみていきたいと思います。
useSyncExternalStoreとは?
機能について
React公式ドキュメントによると、一番上(目立つ所)には、外部ストアへのサブスクライブを可能にするフックと説明がされています。
ただドキュメントを読み進めると、時間とともに変化する、ブラウザが公開する値にサブスクライブしたい場合にも使えるよ。という説明があります。
今回は、2つ目の useSyncExternalStore
の、Reactのレンダリングサイクルの外にある要素の変更を検知して、Reactのレンダリングのサイクルとに橋渡しをしてくれる、ブラウザAPIとの連携について見ていきます。
引数について
3つの引数を持っています、それぞれの引数の役割について見ていきましょう。
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
subscribe(変更を監視する関数)
subscribe
は登録されたイベントリスナーの変更を検知したら、callback()
を呼び出して、状態が変更されたことをReactに知らせます。
getSnapshot(常に最新の値を返す関数)
Reactは、getSnapshot
が返り値を確認して、値が前回と異なる場合にのみコンポーネントを再レンダーします。
getServerSnapShot(SSRやHydration中の初期値を返す関数)
クライアントの準備が完了するまでは getServerSnapshot
の値を使ってレンダリング処理を行う。
動作の流れを確認する
大きく分けてSSR・ハイドレーション中 → getSnapshotを使った最初の表示というクライアントの準備が出来るまで。 その後のイベントの監視・callbackの実行 → getSnapshotの実行、返り値に応じて再レンダリングのループ。 という2つに分けて動作を考えてみると良いかも知れません。
全体の流れをまとめた図が以下になります。
フックを実装してみる
では、実際に useSyncExternalStore
を使ったフックを作成していきたいと思います。
今回作成するものは、以下のような window
の横幅に変更があった場合に変更を検知して、横幅を返却する フック を作成していきたいと思います。
export const useMediaWidth = () => {
const windowWidth = useSyncExternalStore(
subscribe,
getSnapshot,
getServerSideSnapshot
);
return windowWidth;
};
const subscribe = (callback: () => void) => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
};
const getSnapshot = () => window.innerWidth;
const getServerSideSnapshot = () => 0;
では、
subscribe
ウィンドウのリサイズイベントを監視、変更があれば callback()
を実行することで、状態が変更されたことをReactに通知しています。
getSnapshot
ブラウザのAPIの window.innerWidth
を返しています。
Reactは、getSnapshot
の値が前回と異なる場合にのみ、再レンダリングします。
getServerSnapshot
クライアント側の準備ができるまでの初期値で、SSR、およびハイドレーション中にのみ使用されます。
useSyncExternalStoreを使う理由
これまで見てきた実装方法は、useEffect
や useState
を使っても、以下のように同じような実装ができますよね。
import { useLayoutEffect, useState } from "react";
export const useWindowWidth = () => {
const [size, setSize] = useState(0);
useEffect(() => {
const updateSize = () => {
setSize(window.innerWidth);
};
window.addEventListener('resize', updateSize);
updateSize();
return () => window.removeEventListener('resize', updateSize);
}, []);
return size;
};
では、useSyncExternalStore
を使う理由やメリットは何でしょうか?
個人的には、以下の点をあげることができると思います。
- React公式が出している、ブラウザ API へのサブスクライブのフック
- フックがSSRのサポート & ハイドレーション中の対応をしてくれる
- 不要なstateを持つ必要がなくなる
- useEffect内の初回の実行がなくなる
Reactの公式が出しているという点・フックがSSR等の状態を管理してくれる点は、使用する際に大きな後押しになるのではないでしょうか?
また、useState
で値を管理する必要がなくなる点や、抽象的ですが直接的なコードをかける。といった面も個人的には好きなポイントです。
まとめ
いかがだったでしょうか、状態管理ライブラリが使っているイメージがあったので難しい印象も持っていましたが、意外と簡単に使う事ができたのではないでしょうか?
まずは個人のプロジェクトから、使える場面では積極的に使っていこうと思います。
参考文献
Discussion
🧠 結局の違いは「タイミング」と「一貫性」にあり
useEffect
)useSyncExternalStore
)useState
+useEffect
useSyncExternalStore
内で処理getServerSnapshot
でサーバーの初期状態を管理可能🔑 結論: 大きな違いはないように見えて重要な理由
useEffect
はレンダリング後に動作しますが、useSyncExternalStore
はレンダリング中に状態を購読し、サーバーとクライアント間の一貫性を維持します。useSyncExternalStore
に統合され、コードが簡潔になります。結局のところ、状態管理の位置を移動しただけのように見えますが、これにより React レンダリングの重要な課題である 「初期レンダリングの一貫性」 を解決できることがポイントです。 🚀