RecoilのLoadableクラスのようなものを作ってSuspenseさせてみる

# はじめに
Reactの状態管理ライブラリの一つである[Recoil](https://recoiljs.org/)には[Loabable](https://recoiljs.org/docs/api-reference/core/Loadable/)というオブジェクトが存在します。
このオブジェクトは主に非同期データを状態として扱う時に利用されます。現在の状態や値、さらにそれに対する操作などが定義されています。
React18以前のバージョンでは非同期データを状態として扱う時によく使われますが、`Suspense`が安定したReact18以降ではあまり使われないです。
この記事では`Loadable`オブジェクトのようなものを実装することで素のReactにおける`Suspense`の扱いが綺麗になるのではと考えて実装したものを紹介します。結論を言うとあまり綺麗なフローを描くことが出来ませんでしたのでこの方法でアプリを書くことはお勧めできません。

Recoilについては[こちらの記事](https://qiita.com/KokiSakano/items/99bfc87d954c3c515ce4)、非同期データの扱いについては[別記事](https://qiita.com/KokiSakano/items/2bbe936aac990eeb295b)をごご覧ください。

# Loadableとは
`Loadable`とは状態の状況を表すオブジェクトです。Recoilの実装は[こちら](https://github.com/facebookexperimental/Recoil/blob/f1d2969e0c2dac25877d051829b18cd6addbd0c9/packages/recoil/adt/Recoil_Loadable.js)です。
このオブジェクトは状況を表す三つの値を持つ`state`と`state`に応じた値を持つ`contents`をプロパティとして持ちます。
`state`は状態が確定していることを示す`'hasValue'`、状態の取得でエラーが起きたことを示す`'hasError'`、状態の取得中であることを示す`'loading'`を持ちます。
`contents`は`state`が`hasValue`の時は状態が持つ値を、`hasError`の時は`Error`オブジェクトを、`loading`の時は解決中の`Promise`を返します。
さらに`Lodable`オブジェクトは10個のメソッドを持っています(安定した機能ではないことに気をつけてください)。`getValue`、`toPromise`、`errorOrThrow`などがありますが、この記事では`valueOrThrow`だけを実装します。`valueOrThrow`は解決済みの値を持っている場合はそれを返却して、読み込み中だった場合は解決中の`Promise`を投げ、解決中にエラーが生じた場合はその`Error`オブジェクトを投げます。

# Loadableの実装
データを取得するための非同期関数`fetcher`を引数とする`Loadable`クラスを作成します。
```ts
type ValueLoadable<T> = {
  state: "hasValue";
  contents: T;
};
type LoadingLoadable<T> = {
  state: "loading";
  contents: Promise<T>;
};
type ErrorLoadable = {
  state: "hasError";
  contents: unknown;
};
export class Loadable<T> {
  loadable: ValueLoadable<T> | LoadingLoadable<T> | ErrorLoadable;
  constructor(fetcher: Promise<T>) {
    this.loadable = {
      state: "loading",
      contents: fetcher
        .then((data) => {
          console.log(1);
          this.loadable = {
            state: "hasValue",
            contents: data
          };
          return data;
        })
        .catch((error) => {
          this.loadable = {
            state: "hasError",
            contents: error
          };
          throw error;
        })
    };
  }
  valueOrThrow(): T {
    switch (this.loadable.state) {
      case "loading":
        throw this.loadable.contents;
      case "hasValue":
        return this.loadable.contents;
      case "hasError":
        throw this.loadable.contents;
    }
  }
}
```
型を精密に持たせる都合上`Loadable`オブジェクトの`loadable`にアクセスして`state`と`contents`を取得できるようにしました。`loadable`の型は`ValueLoadable`、`LoadingLoadable`、`ErrorLoadable`のように`state`ごとに分割しました。
`constructor`ではデータを取得するための非同期関数`fetcher`を受け取って`loadble`に`state`と`contents`を詰めていきます。最初は読み込み中の状態なので`state`に`'loading'`、`content`には`fetcher`に成功後の処理とエラー時の処理を含めて配置しました。追加した成功後の処理とエラー時の処理では`loadble`の再更新を行わせます。成功後の処理では`loadable`の`state`を`'hasValue'`に`contents`を成功結果を渡すようにしています。エラー時の処理では`loadable`の`state`を`'hasError'`に`contents`をエラーを渡すようにしています。これによって`Loadable`を作成したタイミングで処理が走り、完了後は成功または失敗の状態に陥る処理を作ることが出来ました。
そして、最後に`state`が`'hasValue'`の時だけ`contents`を返してそれ以外の時は`contents`を投げるようなメソッド`valueOrThrow`を定義しています。型を適切に定義したので分岐させているだけですね。

# Suspenseを試す
この`Loadable`オブジェクトを利用して`Suspense`させるコンポーネントを実装してみます。
```tsx:App.tsx
export default function App() {
  const [loadable] = useState(() => new Loadable(fetcher));
  return (
    <div>
      <Suspense fallback={<p>Loading...</p>}>
        <Value loadable={loadable} />
      </Suspense>
    </div>
  );
}
```
```tsx:Value.tsx
export const Value = <T extends string | number>({
  loadable
}: {
  loadable: Loadable<T>;
}): JSX.Element => {
  const value = loadable.valueOrThrow();
  return <>{value}</>;
};
```
https://codesandbox.io/embed/suspense-vlzuwx?fontsize=14&hidenavigation=1&theme=dark

この例では親コンポーネントで`Loadable`を呼び出して、その子コンポーネントの`Value`で`valueOrThrow`を使って解決するまでLoading画面を出すようにしています。
このようにデータを扱うためには`Loadable`を親のコンポーネントで呼び出して、扱うコンポーネントに渡して`valueOrThrow`を発火させると言う仕組みを組む必要があります。コンポーネントでデータを扱うために親で呼び出す必要があるのはあまり良い仕組みとは考えられません(それが良い時もありますが、強制されるのは良くないと考えています)。ここがはじめにで話したお勧めできない理由です。
このようになる理由は`Suspense`が解決した後はそのコンポーネントをレンダリングし直すので、同じコンポーネントで`Loadable`を作成した場合は解決するたびに`Loadable`が作り直されてまたデータの読み込みが始まりSuspenedされてしまうように無限ループに陥るからです。そのため、ただ親に置けばいいだけではなく`Suspense`はからならず親に書く必要があります。


# まとめ
素のReactで綺麗にSuspenseを使いたかったのですが、残念ながらできませんでした。`Loadable`の作り込みが甘いことが原因の可能性もあるのでそこを強化してさらなる改善を積んでみます。