useSyncExternalStoreを使ったSSRの判定
こんにちは。株式会社AI ShiftでWebフロントエンドエンジニアをしている安井です。今回はReact Ariaのソースコードを眺めていたときに発見した、useSyncExternalStore
を活用したSSRの判定について紹介します。
一般的なSSRの判定方法
useIsSSRはサーバーサイドレンダリング中もしくはハイドレーション中かどうかを判定するフックです。
import { useIsSSR } from 'react-aria';
function MyComponent() {
const isSSR = useIsSSR();
return <span>{isSSR ? 'Server' : 'Client'}</span>;
}
一般的に、サーバーサイドレンダリング(SSR)かどうかを判定する方法としてtypeof window === 'undefined'
を使う実装があります。ブラウザ環境ではwindow
オブジェクトが存在しますが、Node.jsなどのサーバー環境には基本的に存在しません。そのためwindow
が定義されているかどうかを基準にSSRか否かを判定します。
React Ariaでは、document
オブジェクトの有無をフラグとしてSSRを判定しています。そのうえで、useContextを利用してグローバルに状態を共有し、useLayoutEffectによってハイドレーション完了後に状態を更新します。一見するとこの実装で十分に見えますが、現在はReact 18以前のバージョン専用のLegacy実装と位置づけられています。
useSyncExternalStoreを使用したSSRの判定
代わりに利用されているのがReactのuseSyncExternalStore
です。useSyncExternalStore
を使ったケースでは、useContext
の状態管理やuseLayoutEffect
による副作用の発火がないことがわかります。
useSyncExternalStoreとは
ではどのようにしてSSRの判定をしているのでしょうか。まずは、useSyncExternalStore
フックの仕様を整理します。
useSyncExternalStore
は外部ストアへのサブスクライブを可能にするReactのフックであり、3つの引数を受け取ります。
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
- subscribe(必須)
- ストアへのサブスクライブを開始する関数。
- callbackを引数に受け取り、ストアが変更されたらそのcallbackを呼び出す。
- callbackが呼ばれるとReactはgetSnapshotを実行する。
- getSnapshot(必須)
- ストアのスナップショットを返す関数。
- ストアが変わっていなければ、呼び直しても必ず同じ値を返す必要がある。
- 値が変わった場合、Reactはコンポーネントを再レンダリングする。
- getServerSnapshot(省略可能)
- サーバーでの初期スナップショットを返す関数。
- サーバーレンダリング中、またはクライアントでのハイドレーション時にのみ使用される。
- 省略した場合、サーバー上でのレンダリングはエラーになる。
useSyncExternalStoreを使った例
まずは通常のuseSyncExternalStoreの利用例を見ていきます。
Reactのドキュメントに記載されているブラウザAPIへのサブスクライブを目的としたuseSyncExternalStore
の利用が参考になります。
import { useSyncExternalStore } from 'react';
export default function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
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);
};
}
ここではブラウザのonline/offlineイベントに対してリスナーを登録し、状態の変化を検知します。イベントが発火するとcallbackが呼ばれ、ReactはgetSnapshotを再評価して最新の接続状態を取得します。その結果が前回と異なれば、コンポーネントが再レンダリングされ、UI上に最新のオンライン/オフライン状態が反映されます。
React Ariaではどう活用しているのか
次にReact AriaがどのようにuseSyncExternalStore
を活用してSSRの判定をしているのか見ていきます。すると、subscribe処理は特に実装せず、getSnapshotにfalseを、getServerSnapshotにtrueを設定していることがわかります。
先ほど解説した通り、第3引数のgetServerSnapshotはサーバーレンダリング中、またはクライアントでのハイドレーション時に使用されます。そしてクライアントでハイドレーション完了後にgetSnapshotの結果が使用される特性を活かしてSSRの判定をしている実装になります。
typeof window === 'undefined'
では不十分なのか?
なぜReact AriaがuseSyncExternalStore
を活用してSSRの判定をしていることはわかりました。次に浮かぶ疑問としては、なぜtypeof window === 'undefined'
では不十分なのか?ということです。
そもそも、React AriaのLegacyな実装でもwindow
ではなくdocument
オブジェクトの有無をフラグとして判定していました。window
からdocument
を使用するように修正された該当のPRはこちらです。
添付された内容によると、Node.js環境ではtypeof window === 'undefined'
による判定は問題なく動作します。しかしDeno 1.x系ではwindow
オブジェクトが定義されているため、この方法では誤解を招く可能性があると指摘されています。
実際、2025/09時点のバージョンではDenoからwindowオブジェクトは削除されているためtypeof window === 'undefined'
による判定でも問題なく動作します。
一方で、現在のJavaScriptのランタイムはNode.jsに加えてDenoやBun、それら以外にも多数存在します。その観点ではランタイムによる差異で発生するエッジケースを踏まないためにも、useSyncExternalStore
を用いたSSRの判定を利用した方が余計な関心を持たなくて済むというのが率直な感想です。
まとめ
今回はReact Ariaのソースコードを読んでいた時に見つけたuseIsSSRというフックを起点に、useSyncExternalStore
の特徴とSSRの判定の方法について紹介しました。
useSyncExternalStore
を活用することによって、グローバルな状態管理や副作用を発火させることなくSSRの判定を管理することができるようになります。また、JavaScriptのランタイムによる差異を意識する必要がなくなる点もメリットであると感じました。
最後に
AI Shiftではエンジニアの採用に力を入れています!
少しでも興味を持っていただけましたら、カジュアル面談でお話しませんか?
(オンライン・19時以降の面談も可能です!)
【面談フォームはこちら】
Discussion