ReactHooksについてまとめる（Other Hooks useSyncExternalStore ）

# 概要
ReactHooksのOther Hooksについてのメモ記事です。基本的にReactの公式ドキュメントの咀嚼記事となっています。

今回はuseSyncExternalStore編です。

## useSyncExternalStore
useSyncExternalStoreは、外部ストアをサブスクライブできるようにするReact Hookです。

### 外部ストアをサブスクライブする

ほとんどのReactコンポーネントは、props、state、contextからしかデータを読みません。しかし、時にはコンポーネントがReactの外部にあるストアから、データを読み込む必要があります。

- Reactの外部に状態を保持するサードパーティの状態管理ライブラリ。
- Reactの外部に状態を保持するサードパーティの状態管理ライブラリ。ミュータブルな値とその変更を購読するためのイベントを公開するブラウザAPI。


外部データストアから値を読み込むには、コンポーネントの最上位で useSyncExternalStore を呼び出します。



ストア内のデータのスナップショットを返します。引数として2つの関数を渡す必要があります。

1つ目は、subscribe関数はstoreに登録し、登録を解除する関数を返します。
2つ目は、getSnapshot関数は、ストアからデータのスナップショットを読み込む必要があります。
Reactはこれらの関数を使用して、コンポーネントをストアに登録したままにし、変更時に再レンダリングします。


以下のように、トップレベルの変数にを管理するtodoStoreがあるとします。ここには、subscribe関数とgetSnapshot関数を登録します。
```todoStore.js
let nextId = 0;
let todos = [{ id: nextId++, text: 'Todo #1' }];
let listeners = [];

export const todosStore = {
  addTodo() {
    todos = [...todos, { id: nextId++, text: 'Todo #' + nextId }]
    emitChange();
  },
  subscribe(listener) {
    listeners = [...listeners, listener];
    return () => {
      listeners = listeners.filter(l => l !== listener);
    };
  },
  getSnapshot() {
    return todos;
  }
};

function emitChange() {
  for (let listener of listeners) {
    listener();
  }
}
```

TodosApp コンポーネントは、useSyncExternalStore フックでその外部ストアに接続します。
```App.js
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

export default function TodosApp() {
  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
  return (
    <>
      <button onClick={() => todosStore.addTodo()}>Add todo</button>
      <hr />
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>
  );
}
```

### ブラウザAPIをサブスクライブする

ブラウザによって、変化する情報をサブスクライブする場合にも使用できます。
たとえば、ネットワーク接続が有効であるかどうかをコンポーネントに表示させたい場合があるとします。その際には `navigator.onLine`というプロパティでブラウザから情報を取得できます。このような値はReactの知らない所で変更される可能性があるので useSyncExternalStoreで読み取ります。

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

function ChatIndicator() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  // ...
}
```

現在のネットワーク接続を取得するために、`getSnapshot`の戻り値として`navigator.onLine`を設定します。
```jsx
function getSnapshot() {
  return navigator.onLine;
}
```

次にsubscribe関数を実装する必要があります。
例えば、navigator.onLineが変化すると、ブラウザはwindowオブジェクトに対してonlineとofflineのイベントを発生させます。callback 引数を対応するイベントにサブスクライブし、サブスクリプションをクリーンアップする関数を返す必要があります。

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

これで、Reactは、`navigator.onLine` APIから値を読み取る方法と、変更をサブスクライブできるようになりました。

```App.js
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://codesandbox.io/embed/sleepy-hooks-lzc85p?fontsize=14&hidenavigation=1&theme=dark


## 参考
https://beta.reactjs.org/apis/react/useSyncExternalStore