Recoilにおける非同期データの扱い方

# はじめに
Reactの状態管理ライブラリとしてMetaが開発しているRecoilがあります。Recoilは宣言的という点に重きにおいて開発している点や、Reactが提供するhooks　APIのような書き心地ですので普段Reactを利用するユーザーにとってはとても親しみやすく感じるライブラリです。
この記事ではそんなRecoilで非同期データを扱う方法を紹介します。Recoil自体については以前記事を書いたので[こちら](https://qiita.com/KokiSakano/items/99bfc87d954c3c515ce4)も合わせてご覧ください。

# 基本のおさらい
Recoilは`atom`と`selector`の二つの概念によって状態を構築します。
## atom
`atom`はコンポーネントがサブスクライブする単位です。これ以上分離できない状態ということなので、命名にふさわしい性質を持っていますね(原子は陽子、中性子、電子からなっていてそれらもクォークなどの細かい粒子で構成されるというのは置いておいてある程度巨視的な視点においてです)。
```ts
const countState = atom<number>({
  key: 'count',
  default: 0,
});
```

## selector
`selector`は`atom`もしくは別の`selector`を利用して作られる状態です。構成する関数は純粋関数である必要があり、利用している状態が更新されると`selector`も更新されます。`selector`をサブスクライブしたコンポーネントはその`selector`自身に変化があった時のみ再レンダリングされます(`selector`が利用している状態には依らない)。
```ts
const isOddCountState = selector<boolean>({
  key: 'isOddCount',
  get: ({ get }) => {
    const count = get(countState);
    return count % 2 === 1;
  },
});
```

## データの取り扱い
データを扱う基本的なhooksとしては`useRecoilState`、`useRecoilValue`、`useSetRecoilState`があります。
`useRecoilState`はReactの`useState`と同じような返り値を返します。引数に渡すのは状態の初期値ではなく状態であることに注意してください。
```ts
const [count, setCount] = useRecoilState(countState);
```
Reactでは`useState`を用いて状態の定義とgetterとsetterの取得をまとめて行っていたのに対して、Recoilでは`atom`や`selector`を用いて状態の定義を行い、`useRecoilState`を用いてgetterとsetterを取得します。宣言と利用が分かれたのでコード全体の見通しが良くなりました。
`useReacilValue`はgetterだけを取得します。サブスクライブだけしたい場合はこれを利用してください。
```ts
const count = useRecoilValue(countState);
```
`useSetRecoilState`はsetterだけを取得します。状態をサブスクライブしないので、これを利用するコンポーネントでは指定した状態が更新されても再レンダリングは起きません。パフォーマンス上の利点が大きいので積極的に利用してください。
```ts
const setCount = useSetRecoilState(countState);
```

## family
Recoilで扱える状態には`atom`と`selector`を発展させた`atomFamily`と`selectorFamily`も存在します。基本的な性質は`atom`や`selector`と同じですが、familyはパラメータを持つことができます。`atom`や`selector`は一つの状態につき一つの値を持ちますが、familyはパラメータごとに値を持たせることができます。
基本の形とほとんど変わらずに定義することができます。
```ts
const countState = atomFamily<number, { id: number }>({
  key: 'count',
  default: ({ id }) => 0,
});
```
状態を扱うときもほとんど変わりません。
```ts
const [count,  setCount] = useRecoilState(countState({ id: 0 }));
```
この例では`countState`が`id`ごとに異なる値を持ちます。`id`が`0`の状態を`1`に更新したのちに、`id`が`1`の状態を確認してもデフォルト値の`0`が得られます(`0`の状態を確認するともちろん`1`です)。データ一覧に対するデータの詳細を状態として定義したいときにidごとにデータを持つことができるように、状態の表現の幅が広がります。

## useRecoilCallback
`useRecoilCallback`は名前の通りReactの`useCallback`と似た機能を持つhooksです。
このhooksではこの関数内だけで状態を利用することが可能です。利用する状態はサブスクライブされますが、更新されてもコンポーネントの再レンダリングは起きません。
下のように引数には`callback`関数と`deps`依存配列を取ります。
```ts
useRecoilCallback(callback, deps)
```
`deps`の機能は`useCallback`と同じなので省略します。`eslint`の設定を下記のように追加すると、依存配列を正しく設定できるのでお勧めです。
```json
{
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": [
      "warn", {
        "additionalHooks": "useRecoilCallback"
      }
    ]
  }
}
```
`callback`では最初に状態に対する操作のための関数を引数で選択します。次にこの関数を利用する側が渡す引数を設定します。その後、それらを使って行いたいことを記述します。操作のために提供される関数は6つあります。`snapshot`はアクセスした時の状態の`Snapshot`オブジェクトを取得することができます。`gotoSnapshot`は状態を`Snapshot`に同期させます。`set`は状態を更新します。`reset`は状態をデフォルトの状態にリセットします。`refresh`は`selector`のキャッシュをリセットします(セレクターは非同期データを扱うときに有効的です)。`transact_UNSTABLE`はUNSTABLEなので省略します。`Snapshot`オブジェクトは状態に対して行える様々な機能を持っていますが、`useRecoilCallback`でよく使われるのは`getPromise`と`getLoadable`です。`getPromise`は状態の値を非同期で取得することができます。非同期であることによって状態が扱うデータが同期か非同期かを気にする必要がなくなるというわけです。
```ts
const logCount = useRecoilCallback({ snapshot }) => async () => {
  const count = await sanpshot.getPromise(countState);
  console.log(`現在のカウント数:${count}`);
}, []);
```
この関数をボタンなどに仕込んであげると、`countState`をサブスクライブせずにログを吐かせることが出来ます。


他にも色々な機能がありますが、おさらいはこの程度にしておきます。

# 非同期データを扱う
Recoilでは非同期データを状態として扱うことができます。
一番簡単な例としてタスクの一覧を取得できる非同期関数`getTasks`を用いて`atom`を作成します。
```ts
const tasksState = atom<Task[]>({
  key: 'taskList',
  default: getTasks(),
});
```
この例では`default`に非同期関数が渡ったこと以外は同期的に状態を扱う時と見た目はほとんど変わりません。これらのデータを扱うにはSuspenseを使うケースとそうでないケースに分かれます。

## Suspense
`Suspense`を利用する場合は同期的な関数とほとんど変わらない方法で利用することができます。
```tsx
const TaskList = (): JSX.Element => {
  const tasks = useRecoilValue(tasksState);

  return (
    <>
      {tasks.map((task) => <Task key={task.id} task={task} />)}
    </>
  );
}
```
状態が扱うデータが同期、非同期を気にせずに利用できるのでとても利用しやすいです。
`Suspense`を使う場合は当然ですが設置を忘れた時のためにルートにも設置してください。全ての祖先で`Suspense`を設置していないコンポーネントで非同期データを扱う状態を利用するとエラーが出るので気をつけてください。`RecoilRoot`の配下に設置しておくのが一番安全です。
```tsx
return (
  <RecoilRoot>
    <Suspense fallbask={<Loading />}>
      {children}
    </Suspense>
  </RecoilRoot>
);
```
`Suspense`を利用するととても快適ですが、非同期関数でエラーを吐いた時の処理が大変です。一番簡単な方法は`ErrorBoundary`を利用する方法です。
```tsx
return (
  <RecoilRoot>
    <Suspense fallbask={<Loading />}>
      <ErrorBoundary>
        {children}
      </ErrorBoundary>
    </Suspense>
  </RecoilRoot>
);
```
この方法は同期的な状態、読み込み中の状態、エラーの状態の全てをきれいに管理することができるのでおすすめです。
`ErrorBoundary`を使わない場合は状態として同期的な状態、エラーの状態の二つを定義して、コンポーネント側で出し分ける必要があります。
```ts
const tasksState = atom<Task[]>({
  key: 'taskList',
  default: () => {
    const response = getTasks();
    if (response.error) {
      return {
        state: 'hasError',
        contents: response.error,
      }
    }
    return {
      state: 'hasValue',
      contents: response.tasks,
    }
  }
});
```
宣言的という特徴が薄れるのであまりお勧めできませんが、ErrorBoundaryを使えない場合はこのようにするか次に紹介する`Suspense`を使わない時の表現を利用すると手段も残されています。

## No Suspense
React18以前で開発をしているなど様々な理由で`Suspense`を利用できないケースがあるかと思います。`Suspense`を用いない場合は状態を扱うhooksが同期的なものと異なります。`useRecoilState`は`useRecoilStateLoadable`に、`useRecoilValue`は`useRecoilValueLoadable`になります。`useSetRecoilState`は状態を設置するだけなので変わりません。これらのhooksは値を`Loadaale`というオブジェクトの形式で扱います。
```ts
const tasksLadable = useRecoilValueLoadable(tasksState);
```
`Loadable`は`state`と`contents`の二つのプロパティを持ちます。`state`は`hasValue`、`hasError`、`Loading`など現在の状態の状況を持ちます。`contents`は`state`によって異なる値を持ちます。`hasValue`であれば取得したデータの状態、`hasError`であればエラーオブジェクト、`loading`であれば`Promise`を持ちます。`Loadable`は様々な機能を持ちますが安定した機能ではないのでここでは紹介しないことにします。`state`と`contents`があれば、基本的には困らないはずです。
`Loadbale`はコンポーネントで以下のように利用します。
```tsx
const TaskList = (): JSX.Element => {
  const tasksLoadable = useRecoilValue(tasksState);
  if (tasksLoadable.state === 'loading') {
    return <Loading />
  }
  if (tasksLoadable.state === 'hasError') {
    throw tasksLoadable.contents
  }

  return (
    <>
      {tasks.contents.map((task) => <Task key={task.id} task={task} />)}
    </>
  );
}
```
他にも`useEffect`を用いて非同期状態を取得したのちに状態をセットするような方法が考えられますが避けた方が良いと考えています。その状態を扱う各コンポーネントで`useEffect`の処理を書く必要があり可読性が悪くなることや、状態が持つ型が扱いづらいものになることなどのデメリットが考えられるからです。

## 状態の更新
非同期データを扱っているときはDataBaseの値など、外部ソースのデータを参照していることが多いです。Recoilでは状態をグローバルに持つのでデータの再取得はリロードを行う限りされません。これでは古いデータを持ち続けたままユーザーに利用させてしまいます。何らかのタイミングでデータの再取得を行いデータをある程度新しいものに更新したいです。Recoilの提供するhooksに`useResetRecoilState`があります。これは状態を初期化するhooksです。
```ts
const reset  = useResetRecoilState(tasksState);
```
のように定義されます。これを使えば更新できそうですが、残念ながら状態の初期化はされますが新しいデータを受け取ることはできません。最初に行った関数の結果が返ってくるだけです。これは`reset`を実行しても保存していたデフォルトの値に置き換えているだけなのが原因ではないかと考えています(ソース見ていないので推測です)。
これを解決する3つの策があります。一つ目はリクエスト回数を表す状態を作ることです。
```ts
const requestId = atom<number>({
  key: 'requestId',
  default: 0,
});
const tasks = selector<Task[]>({
  key: 'tasks',
  get: async ({ get }) => {
    get(requestId);
    const await response = getTasks();
    return response.tasks;
  }
});
```
`selector`は依存した状態が変化するごとに計算し直すので、`requestId`を変化させる(+1する)ごとにデータを新しく読み込んでくれます。しかし、この方法は原始的であまり行いたくありません。
二つ目は`useRecoilRefresher_UNSTABLE`を用いることです。`selector`を構成する関数は純粋関数なので依存する状態が変わらなければその状態は変わらないはずです。その信念をもとに`selector`の結果はキャッシュされています。つまりこのキャッシュを削除すれば再度関数を実行する必要が出てくるので新しいデータを得ることが可能になります。キャッシュの削除のための関数が`useRecoilRefresher_UNSTABLE`です。
```ts
const tasksState = selector<Task[]>({
  key: 'taskList',
  get: async () => {
    const response = await getTasks();
    return response.tasks;
  }
});
```
のように`selector`を定義してデータの再読み込みをしたい箇所で
```ts
const refresh = useRecoilRefresher_UNSTABLE(tasksState);
```
のように宣言して得られる`refresh`を実行するとデータの読み込みが開始されます。他の状態に依存しないのに`selector`を使う点や、このhooksがUNSTABLEなのが懸念点として挙げられます。
最後に`atom`を更新する方法です。任意のタイミングでデータの読み込みを行なってその結果に`atom`を更新する方法です。hooksを作成して提供することでシンプルに書くことが出来ます。
```ts
const useRefreshTasks = () => {
  const refreshTasks = useRecoilCallback(({ set }) => async () => {
    const response = await getTasks();
    set(tasksState, response.tasks);
  }, []);

  return refreshTasks;
};
```
拡張性もあるので、`useRecoilRefresher`がUNSTABLEのうちはこれがベストな方法ではないかと考えています。

## データの事前読み込み
Recoilでは非同期データを扱う状態の事前読み込みを行うことができます。
```ts
const handleChangeTask = useRecoilCallback({ snapshot, set }) => (id: number) => {
  snapshot.getLoadable(taskState(id))
  set(taskId, id);
});
```
この例では、表示するタスクの変更と同時に変更後のタスクの読み込みを開始させています。

# さいごに
Recoilで非同期データと扱う方法を紹介しました(見返すとおさらいが長いのでほんとに非同期の話しているか疑問に感じました)。非同期データの扱いやすさという面ではSWRなどのデータ取得ライブラリには及びませんが、グローバルな状態を一つのライブラリで管理できるという視点では悪くないのではないでしょうか。UNSTABLEな機能が安定するとより使いやすくなると思うので安定化が楽しみです。