useSyncExternalStore について少しまとめてみた

## useSyncExternalStore とは
React18 で新たに追加された React フックで、外部ストアをサブスクライブできるようにする React Hook


## どんな時に使えるか考察
> 外部ストアをサブスクライブできるようにする React Hook

**上記だとあまりピンと来なかったのでもう少し調べてみると、[ドキュメント](https://react.dev/reference/react/useSyncExternalStore#subscribing-to-an-external-store)に以下のようなことが書かれていました。**


>React コンポーネントの多くは props、state、context からデータを参照します。
ただし、React 外部のデータソースから値を参照する必要がある場合も存在します。
このような場合、 useSyncExternalStore を使うことによって、 React 外部のデータソースのストアを監視し、ストアの値を参照できます。
>
>例 )
React の外部で状態を保持するライブラリ
変更可能な値とその変更をサブスクライブするブラウザのイベント API 



**正直、まだピンと来なかったので、もう少し[ドキュメント](https://react.dev/reference/react/useSyncExternalStore#subscribing-to-an-external-store)を読んでみて以下の記述を見つけました。**

1.  `useState` や `useReducer` を使用してReact で状態管理ができるなら、そちらを使用することをお勧めします。`useSyncExternalStore` API は、既存の非 React コードと統合する必要がある場合に主に役立ちます。
2.  `useSyncExternalStore` を追加するもう 1 つの理由は、ブラウザーによって公開されて、状態変化する値をサブスクライブする場合です。



## 利用場面
色々調べたことを少し自分なりに解釈する下記のようになりました。
なんとなくイメージがしやすくなったかと思います。

- **非Reactコードと統合して状態管理をする必要がある場合**
    - `useState` / `useReducer` / ライブラリ 等を用いても React で状態管理ができない場合
- **React を通さずにブラウザ側で起こった変化を管理する場合**


## 使い方
今回は **React でも検知が難しいブラウザの状態管理イベントを読み取る場合** で、ドキュメントにも記載がある、ネットワーク接続がアクティブかどうかをコンポーネントに表示したい場合をやっていきます。


:::note info
ブラウザは、**navigator.onLine** というプロパティを介してネットワーク接続の状態を公開します。
そしてこの値は、React の知らないうちに変更される可能性があるため、**useSyncExternalStore** を使用して読み取る必要があります。
:::




まずはネットワークの状況を、ページに表示するための枠組みを作ります。
パラメータや戻り値に関しての詳細は、[こちら](https://react.dev/reference/react/useSyncExternalStore#parameters)を参照ください。

```react
import { useSyncExternalStore } from 'react';

function useOnlineStatus() {
  // subscribe: ストアが変更されるたびに呼び出されるコールバックを登録する
  // getSnapshot: 現在のストアの値を返す
  return useSyncExternalStore(subscribe, getSnapshot);
}

export default function App() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
```

`getSnapshot` 関数を実装するには、ブラウザ API から現在の値を読み取ります。

```react
function getSnapshot() {
  return navigator.onLine;
}
```

`subscribe` 関数を実装するには、コールバック引数を対応するイベントに登録し、返り値には登録を解除する関数を返す必要があります。
`navigator.onLine` が変更されると、ブラウザはウィンドウオブジェクトでオンラインとオフラインのイベントを発生させます。


```react
function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}
```

`subscribe`、`getSnapshot` を useSyncExternalStore の引数に渡すことで、 `navigator.onLine` の値を読み取り、変更をサブスクライブできます。結果として True か False を返します。

```react
import { useSyncExternalStore } from 'react';

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function getSnapshot() {
  return navigator.onLine;
}

function useOnlineStatus() {
  return useSyncExternalStore(subscribe, getSnapshot);
}

export default function App() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
```


## 番外編

ちなみに、`useEffect` を使って実装するとこのようになります。
ただ今回説明したように、React 18以降は `useSyncExternalStore` を使っていくのが良さそうです。

```react
import { useState } from 'react';

function useOnlineStatus() {
  // 理想的ではありません
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function updateState() {
      setIsOnline(navigator.onLine);
    }

    updateState();

    window.addEventListener('online', updateState);
    window.addEventListener('offline', updateState);
    return () => {
      window.removeEventListener('online', updateState);
      window.removeEventListener('offline', updateState);
    };
  }, []);
  return isOnline;
}

export default function App() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
```

(`useEffect` で実装した場合に、`useSyncExternalStore`と比べてどのようなデメリットがあるかまで細かくは調べきれていないのと、少し時間がかかりそうなので、また追記したいと思います。。。)

ただ可読性については、`useSyncExternalStore` の方がだいぶ良さそうに思います！