useSyncExternalStore 完全入門【React】外部ストアへのサブスクライブ

ReactのuseSyncExternalStoreとは何か、Reactの外部で管理されるデータ(ブラウザAPI・外部ライブラリ)をコンポーネントで安全に購読する方法を解説します。

#react#hooks#usesyncexternalstore#javascript#frontend

Reactでは通常、useStateuseReducer で状態を管理します。しかし、Reduxのような外部ライブラリやブラウザのAPIなど、Reactの外部で管理されるデータを使いたい場面があります。

useSyncExternalStore は、こうした外部データソースに安全にサブスクライブするためのフックです。

この記事でわかること:

  • useSyncExternalStore が必要な理由
  • 3つのパラメータの役割
  • ブラウザAPIへの接続例
  • カスタムフックとしてのラップパターン

useSyncExternalStore とは?

useSyncExternalStore は、React の外部にあるデータストアを購読(サブスクライブ)するフックです。

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?);
パラメータ説明
subscribeストアへのサブスクライブを開始し、クリーンアップ関数を返す
getSnapshotストアの現在のデータを返す関数
getServerSnapshot(省略可能)SSR 用の初期スナップショットを返す関数

戻り値はストアの現在のスナップショット(最新の値)です。

なぜ useEffect だけではダメなのか?

外部ストアのサブスクライブを useEffect だけで実装すると、並行レンダリングにおいて**データの不整合(ティアリング)**が起きる可能性があります。useSyncExternalStore はこの問題を安全に解決します。

実用例1:ネット接続状態の監視

import { useSyncExternalStore } from "react";
 
// ① スナップショット取得関数
function getSnapshot() {
  return navigator.onLine;
}
 
// ② サブスクライブ関数(変化を監視し、クリーンアップ関数を返す)
function subscribe(callback) {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
 
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
}
 
function NetworkStatus() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
 
  return (
    <p style={{ color: isOnline ? "green" : "red" }}>
      {isOnline ? "オンライン" : "オフライン"}
    </p>
  );
}

実用例2:ブラウザのスクロール位置

import { useSyncExternalStore } from "react";
 
function getScrollSnapshot() {
  return window.scrollY;
}
 
function subscribeScroll(callback) {
  window.addEventListener("scroll", callback);
  return () => window.removeEventListener("scroll", callback);
}
 
function ScrollIndicator() {
  const scrollY = useSyncExternalStore(subscribeScroll, getScrollSnapshot);
 
  return (
    <div style={{ position: "fixed", top: 0, right: 0, padding: "8px" }}>
      スクロール位置: {scrollY}px
    </div>
  );
}

カスタムフックとしてラップする

ロジックを再利用可能にするため、カスタムフックとしてまとめるのが一般的なパターンです。

import { useSyncExternalStore } from "react";
 
// カスタムフック:オンライン状態を提供する
function useOnlineStatus() {
  return useSyncExternalStore(
    subscribeOnlineStatus,
    getOnlineSnapshot,
    () => true // SSR時はtrueを返す
  );
}
 
function subscribeOnlineStatus(callback) {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
}
 
function getOnlineSnapshot() {
  return navigator.onLine;
}
 
// どこからでも使い回せる
function Header() {
  const isOnline = useOnlineStatus();
  return <header>{isOnline ? "● オンライン" : "○ オフライン"}</header>;
}

重要な注意点

getSnapshot は安定した参照を返す

// ❌ NG:毎回新しいオブジェクトを返すと無限ループになる
function getSnapshot() {
  return { data: store.data }; // 毎回新しいオブジェクト
}
 
// ✅ OK:変化がなければ同じ参照を返す
let cachedSnapshot = null;
function getSnapshot() {
  if (store.data !== lastData) {
    cachedSnapshot = { data: store.data };
    lastData = store.data;
  }
  return cachedSnapshot;
}

subscribe は安定した関数を使う

// ❌ NG:コンポーネント内で定義すると毎回再サブスクライブされる
function MyComponent() {
  // subscribe がレンダリングのたびに新しい関数になる
  const data = useSyncExternalStore(
    (callback) => { /* ... */ }, // ← 毎回新しい関数
    getSnapshot
  );
}
 
// ✅ OK:コンポーネントの外に定義する
function subscribe(callback) { /* ... */ }
 
function MyComponent() {
  const data = useSyncExternalStore(subscribe, getSnapshot);
}

どんな場面で使うか

場面使うフック
React 内の状態管理useState / useReducer
Reactの外部のストアuseSyncExternalStore
データフェッチ・タイマーuseEffect

Redux などの状態管理ライブラリは内部で useSyncExternalStore を使うことが推奨されています。ライブラリを使う側では直接触れる機会は少ないですが、ブラウザAPIをReactコンポーネントで扱うときに便利です。

まとめ

  • useSyncExternalStore はReact外部のデータストアを安全に購読するフック
  • subscribegetSnapshotgetServerSnapshot の3つのパラメータを持つ
  • ブラウザAPI(オンライン状態・スクロール位置など)との連携に使える
  • カスタムフックとしてラップして再利用するのが一般的パターン
  • getSnapshot は同じデータなら同じ参照を返すようにする

関連記事