Gemcook Tech Blog
🫨

初めてのuseSyncExternalStore

2025/02/04に公開1

はじめに

皆さんは、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 の横幅に変更があった場合に変更を検知して、横幅を返却する フック を作成していきたいと思います。

useMediaWidth.tsx
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を使う理由

これまで見てきた実装方法は、useEffectuseState を使っても、以下のように同じような実装ができますよね。

useMediaWidth.tsx(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 で値を管理する必要がなくなる点や、抽象的ですが直接的なコードをかける。といった面も個人的には好きなポイントです。

まとめ

いかがだったでしょうか、状態管理ライブラリが使っているイメージがあったので難しい印象も持っていましたが、意外と簡単に使う事ができたのではないでしょうか?
まずは個人のプロジェクトから、使える場面では積極的に使っていこうと思います。

参考文献

https://ja.react.dev/reference/react/useSyncExternalStore

Gemcook Tech Blog
Gemcook Tech Blog

Discussion

ピン留めされたアイテム
JoonenJoonen

🧠 結局の違いは「タイミング」と「一貫性」にあり

項目 従来の方法(useEffect 改善された方法(useSyncExternalStore
状態管理の位置 useState + useEffect useSyncExternalStore 内で処理
サーバー/クライアントの一貫性 一貫性の保証が難しい(フラッシュが発生する可能性あり) レンダリング時点で一貫性を保証
初期状態の処理 手動で初期化が必要 getServerSnapshot でサーバーの初期状態を管理可能
レンダリングのタイミング クライアントレンダリング後に状態を更新 レンダリング中に状態を同期(SSR 対応可能)

🔑 結論: 大きな違いはないように見えて重要な理由

  1. レンダリングのタイミング: useEffect はレンダリング後に動作しますが、useSyncExternalStore はレンダリング中に状態を購読し、サーバーとクライアント間の一貫性を維持します。
  2. 状態の一貫性: サーバーとクライアント間の状態差異によるちらつき(flickering)現象を根本的に防ぎます。
  3. コードの一貫性: 状態管理ロジックが useSyncExternalStore に統合され、コードが簡潔になります。

結局のところ、状態管理の位置を移動しただけのように見えますが、これにより React レンダリングの重要な課題である 「初期レンダリングの一貫性」 を解決できることがポイントです。 🚀