react-hooks-asyncの紹介


はじめに

本稿では、React Hooksで非同期処理をするためのライブラリを紹介します。React HooksのuseEffectを使うと、非同期処理の記述は比較的簡単にできます。一方で、useEffectを使う際はcleanup処理が重要ですが、しばしば忘れられることがあります。custom hooksを使うとその辺りを明確にすることができます。

react-hooks-asyncとは

本ライブラリのリポジトリはこちらです。

useEffectのcleanupでは非同期処理を中断することがポイントになります。JavaScriptのPromiseには中断の仕組みは用意されていません。この問題について、redux-thunkやredux-observableはそれぞれのインタフェースを提供して解決しています。

react-hooks-asyncはAbortControllerを使ったインタフェースを提供します。AbortControllerはPromiseではキャンセル処理をできないことに対して、主にFetch API向けに用意されたDOMのAPIです。通常のブラウザでは既に使えるようになっています。polyfillも容易です。react-hooks-asyncで使う非同期関数はAbortControllerのインスタンスを第一引数で受け取る規約とします。つまり、

const asyncFunction = async (abortContoller) => {
  // abortControllerのキャンセル処理を実現するよう処理を書く
  return ...; // 結果を返す
};

のような形になります。

コアAPI

react-hooks-asyncが提供するコアのAPIはuseAsyncTaskuseAsyncRunの二つです。なぜ二つに分かれているかは後ほど説明します。これらのAPIは例えば次のように使います。

const asyncFunction = ...; // 規約に沿った非同期関数

const Component = () => {
  const task = useAsyncTask(asyncFunction);
  useAsyncRun(task);
  return (
    <div>
      <div>Pending: {JSON.stringify(task.pending)}</div>
      <div>Error: {JSON.stringify(task.error)}<div>
      <div>Result: {JSON.stringify(task.result)}<div>
    </div>
  );
};

または、custom hookにすることもできます。ついでに引数を受け付けるようにもしてみましょう。

const useAsyncFunction = (arg1, arg2) => {
  const func = useCallback(async (abortContoller) => {
    // abortContollerの処理
    return ...; // arg1, arg2を使った計算結果など
  }, [arg1, arg2]);
  const task = useAsyncTask(func);
  useAsyncRun(task);
  return task;
};

ヘルパーAPI

useAsyncTaskを使うためは規約に沿ったasyncFunctionが必要ですが、よく使う関数はcustom hooksとしてヘルパーAPIとして用意されています。ヘルパーAPIは現時点では5つあります。

  • useAsyncTaskTimeout
  • useAsyncTaskDelay
  • useAsyncTaskFetch
  • useAsyncTaskAxios
  • useAsyncTaskWasm

今回は、useAsyncTaskFetchについて紹介します。このAPIはFetch APIの引数を基本的にそのままhookの形にしたものです。第一引数はinputで、第二引数はinitで省略可能です。さらに第三引数にreadBody関数を指定できますが、JSON APIの場合は指定不要です。

使用方法は次のようになります。

const Component = ({ userId }) => {
  const url = `https://reqres.in/api/users/${id}?delay=1`;
  const task = useAsyncTaskFetch(url);
  useAsyncRun(task);
  const { pending, error, result } = task;
  return (
    <div>
      <div>Pending: {JSON.stringify(pending)}</div>
      <div>Error: {JSON.stringify(error)}<div>
      <div>Result: {JSON.stringify(result)}<div>
    </div>
  );
};

実は、useAsyncTaskFetchとuseAsyncRunを組み合わせたuseFetchというAPIもあります。これを使うと、

const { pending, error, result } = useFetch(url);

のように簡潔に書くことができ、今回の例ではこれで十分です。

結合API

react-hooks-asyncの特徴として、taskの結合があります。結合のためのAPIは3つあります。

  • useAsyncCombineAll
  • useAsyncCombineReace
  • useAsyncCombineSeq

これらのAPIもAbortControllerを利用する規約に沿っていますので、cleanup処理が正しく実行されます。react-hooks-asyncのREADMEにあるコードを抜粋すると、

const GitHubSearch = ({ query }) => {
  const url = `https://api.github.com/search/repositories?q=${query}`;
  const delayTask = useAsyncTaskDelay(500);
  const fetchTask = useAsyncTaskFetch(url);
  const combinedTask = useAsyncCombineSeq(delayTask, fetchTask);
  useAsyncRun(combinedTask);
  if (delayTask.pending) return <div>Waiting...</div>;
  if (fetchTask.error) return <Err error={fetchTask.error} />;
  if (fetchTask.pending) return <Loading abort={fetchTask.abort} />;
  return ...;
};

のように使うことができます。これはTypeaheadと呼ばれる機能を実現した例で、タイプ中は検索をせず、しばらく経つと検索処理を開始するものです。もし、検索処理中にタイプを再開すると処理を中断します。具体的な動作例は下記になります。

リポジトリに用意されている例があるのでそちらからも試すことができます。

類似のライブラリ

非同期処理をするライブラリは複数ありますが、その中で特に類似するライブラリを紹介します。

react-async

React Hooksが登場する前からあるライブラリで、render propsによるインタフェースも提供されています。また、ドキュメントも充実しています。

react-async-hook

こちらは、React Hooksのインタフェースのみを提供するライブラリです。特徴はdepsの扱いで、useCallbackに依存しなくてもいいような書き方を提案しています。

おわりに

最初にも述べましたが、useEffectを使った非同期処理はcleanup処理さえできるようになれば簡単で、ライブラリを使わなくてもおよそのことは手軽に実現できます。react-hooks-asyncは少し複雑なユースケースで役に立つかもしれませんし、cleanup処理が内包されているので通常利用でも便利かと思います。

ところで、Suspense for Data Fetchingでは全く新しい方法が提案されています。Data Fetchingは一般的な非同期処理よりは狭い話に聞こえますが、Reactにおける非同期処理はデータ取得であることが多いですし、また、あらゆる非同期処理は一種のデータ取得でもあるとみなすこともできるかもしれません。新しい方法はRender-as-You-Fetchと呼ばれ、useEffectを使わない方法です。まだ発展途上ですが、Concurrent Mode/useTransitionによるUXの向上や、通常言われているSuspenseコンポーネントによるDXの向上だけでなく、useEffectで苦労していたdepsやuseCallbackなどの問題が無くなる可能性もあり大変期待しています。Render-as-You-Fetch指向のライブラリも試作していますので、興味ある方はご覧ください。