🐳

Recoil で非同期データを扱う [Suspense, Loadable]

2022/03/10に公開約2,600字

recoil では非同期データの扱い方としてReact.Suspenseを利用する方法と、Loadableを利用する方法が用意されています。
それぞれ用途によって使い分けられますがどっちも便利です。

React.Suspenseと使う

React.Suspense と使えば非同期データが非同期であることを意識することなく扱えます。

atom, selectorにセットされたPromiseが未解決中はReact.Suspenseで指定したフォールバックコンポーネントが描画されます。そのためコンポーネント内では同期データとして扱うことができます。

別途フォールバックコンポーネントの設定は必要ですが、コードを分離できる上に読み込み状態という無駄な状態をコンポーネントから排除できます。

React.Suspense使用の嬉しい点

  • コンポーネント内で未解決時の処理などが不要
  • コンポーネント内ではデータを同期的に扱える

React.Suspenseの使用例

import { atom } from 'recoil';

export const testState = atom<string>({
  key: 'test',
  default: new Promise<string>((resolve) => {
    setTimeout(() => {
      resolve('test');
    }, 10 * 1000);
  }),
});
import * as React from 'react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { testState } from '@src/stores/test';

function TestChild() {
  const test = useRecoilValue(testState);

  return (
    <div>
      <h4>Test Child</h4>
      <h5>current value: {test}</h5>
    </div>
  );
}

export function Test() {
  return (
    <div>
      <h4>Test</h4>
      <RecoilRoot>
        <React.Suspense fallback={<div>Now Loading...</div>}>
          <TestChild />
        </React.Suspense>
      </RecoilRoot>
    </div>
  );
}

Loadableを使う

recoil にはLoadableというデータの状態を表現するためのクラスがあります。
atomselectorの値をLoadableオブジェクトとして受け取ることもできます。

Loadableは現在のデータの状態を表すstateとデータの内容であるcontentsというプロパティを持っています。

  • データが未解決の場合:stateloadingであり、contentsにはPromiseが入っている。
  • データが解決済の場合:statehasValueであり、contentsには解決済みの値が入っている。
  • エラーがあった場合:statehasErrorであり、contentsにはErrorオブジェクトが入っている。

Suspense のフォールバックコンポーネントでは表現が難しい場合や、データの状態によって UI を細かく変化させたり、データの状態によって何か処理をしたい場合はLoadableを使えます。

Loadable使用の嬉しい点

  • すでにstateが準備されているので同期的にコンポーネントを操作できる。
  • すでにstateが準備されているので自前で状態を作らなくて良い。

Loadableの使用例

import { RecoilRoot, useRecoilValueLoadable } from 'recoil';

import { testState } from '@src/stores/test';

function TestChild() {
  const testLoadable = useRecoilValueLoadable(testState);

  if (testLoadable.state === 'hasValue') {
    return (
      <div>
        <h4>Test Child</h4>
        <h5>
          current value:
          {testLoadable.contents}
        </h5>
      </div>
    );
  }
  if (testLoadable.state === 'loading') {
    return <div>Loading...</div>;
  }
  if (testLoadable.state === 'hasError') {
    throw testLoadable.contents;
  }
  throw new ExhaustiveError(testLoadable); // 網羅性チェック
}

export function Test() {
  return (
    <div>
      <h4>Test</h4>
      <RecoilRoot>
        <TestChild />
      </RecoilRoot>
    </div>
  );
}

Discussion

ログインするとコメントできます