🏌️‍♂️

useSyncExternalStoreを使ったSSRの判定

に公開

こんにちは。株式会社AI ShiftでWebフロントエンドエンジニアをしている安井です。今回はReact Ariaのソースコードを眺めていたときに発見した、useSyncExternalStoreを活用したSSRの判定について紹介します。

https://react-spectrum.adobe.com/react-aria/useIsSSR.html

一般的なSSRの判定方法

useIsSSRはサーバーサイドレンダリング中もしくはハイドレーション中かどうかを判定するフックです。

example
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実装と位置づけられています。

https://github.com/adobe/react-spectrum/blob/%40spectrum-icons/workflow%404.2.24/packages/%40react-aria/ssr/src/SSRProvider.tsx#L48-L77

useSyncExternalStoreを使用したSSRの判定

代わりに利用されているのがReactのuseSyncExternalStoreです。useSyncExternalStoreを使ったケースでは、useContextの状態管理やuseLayoutEffectによる副作用の発火がないことがわかります。

https://github.com/adobe/react-spectrum/blob/%40spectrum-icons/workflow%404.2.24/packages/%40react-aria/ssr/src/SSRProvider.tsx#L189-L193

useSyncExternalStoreとは

ではどのようにしてSSRの判定をしているのでしょうか。まずは、useSyncExternalStoreフックの仕様を整理します。

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

useSyncExternalStore外部ストアへのサブスクライブを可能にするReactのフックであり、3つの引数を受け取ります。

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
  1. subscribe(必須)
  • ストアへのサブスクライブを開始する関数。
  • callbackを引数に受け取り、ストアが変更されたらそのcallbackを呼び出す。
  • callbackが呼ばれるとReactはgetSnapshotを実行する。
  1. getSnapshot(必須)
  • ストアのスナップショットを返す関数。
  • ストアが変わっていなければ、呼び直しても必ず同じ値を返す必要がある。
  • 値が変わった場合、Reactはコンポーネントを再レンダリングする。
  1. 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);
  };
}

https://ja.react.dev/reference/react/useSyncExternalStore#subscribing-to-a-browser-api

ここではブラウザのonline/offlineイベントに対してリスナーを登録し、状態の変化を検知します。イベントが発火するとcallbackが呼ばれ、ReactはgetSnapshotを再評価して最新の接続状態を取得します。その結果が前回と異なれば、コンポーネントが再レンダリングされ、UI上に最新のオンライン/オフライン状態が反映されます。

React Ariaではどう活用しているのか

次にReact AriaがどのようにuseSyncExternalStoreを活用してSSRの判定をしているのか見ていきます。すると、subscribe処理は特に実装せず、getSnapshotにfalseを、getServerSnapshotにtrueを設定していることがわかります。

https://github.com/adobe/react-spectrum/blob/%40spectrum-icons/workflow%404.2.24/packages/%40react-aria/ssr/src/SSRProvider.tsx#L170-L182

先ほど解説した通り、第3引数のgetServerSnapshotはサーバーレンダリング中、またはクライアントでのハイドレーション時に使用されます。そしてクライアントでハイドレーション完了後にgetSnapshotの結果が使用される特性を活かしてSSRの判定をしている実装になります。

なぜtypeof window === 'undefined'では不十分なのか?

React AriaがuseSyncExternalStoreを活用してSSRの判定をしていることはわかりました。次に浮かぶ疑問としては、なぜtypeof window === 'undefined'では不十分なのか?ということです。

そもそも、React AriaのLegacyな実装でもwindowではなくdocumentオブジェクトの有無をフラグとして判定していました。windowからdocumentを使用するように修正された該当のPRはこちらです。

https://github.com/adobe/react-spectrum/pull/4688

添付された内容によると、Node.js環境ではtypeof window === 'undefined'による判定は問題なく動作します。しかしDeno 1.x系ではwindowオブジェクトが定義されているため、この方法では誤解を招く可能性があると指摘されています。

https://github.com/remix-run/remix/blob/remix%401.17.1/docs/pages/gotchas.md#typeof-window-checks

実際、2025/09時点のバージョンではDenoからwindowオブジェクトは削除されているためtypeof window === 'undefined'による判定でも問題なく動作します。

https://github.com/denoland/deno/issues/13367

一方で、現在のJavaScriptのランタイムはNode.jsに加えてDenoやBun、それら以外にも多数存在します。その観点ではランタイムによる差異で発生するエッジケースを踏まないためにも、useSyncExternalStoreを用いたSSRの判定を利用した方が余計な関心を持たなくて済むというのが率直な感想です。

まとめ

今回はReact Ariaのソースコードを読んでいた時に見つけたuseIsSSRというフックを起点に、useSyncExternalStoreの特徴とSSRの判定の方法について紹介しました。

useSyncExternalStoreを活用することによって、グローバルな状態管理や副作用を発火させることなくSSRの判定を管理することができるようになります。また、JavaScriptのランタイムによる差異を意識する必要がなくなる点もメリットであると感じました。

最後に

AI Shiftではエンジニアの採用に力を入れています!
少しでも興味を持っていただけましたら、カジュアル面談でお話しませんか?
(オンライン・19時以降の面談も可能です!)

【面談フォームはこちら】

https://hrmos.co/pages/cyberagent-group/jobs/1826557091831955459

AI Shift Tech Blog

Discussion