react-hooks-asyncの紹介

## はじめに

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

## react-hooks-asyncとは

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

<https://github.com/dai-shi/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のインスタンスを第一引数で受け取る規約とします。つまり、

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

のような形になります。

### コアAPI

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

```jsx
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にすることもできます。ついでに引数を受け付けるようにもしてみましょう。

```javascript
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](https://developer.mozilla.org/ja/docs/Web/API/WindowOrWorkerGlobalScope/fetch)の引数を基本的にそのままhookの形にしたものです。第一引数は`input`で、第二引数は`init`で省略可能です。さらに第三引数に`readBody`関数を指定できますが、JSON APIの場合は指定不要です。

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

```jsx
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もあります。これを使うと、

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

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

## 結合API

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

- useAsyncCombineAll
- useAsyncCombineReace
- useAsyncCombineSeq

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

```javascript
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と呼ばれる機能を実現した例で、タイプ中は検索をせず、しばらく経つと検索処理を開始するものです。もし、検索処理中にタイプを再開すると処理を中断します。具体的な動作例は下記になります。

![screencast.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63327/8e696804-a457-5533-7a00-b3276191de62.gif)

[リポジトリに用意されている例](https://github.com/dai-shi/react-hooks-async#examples)があるのでそちらからも試すことができます。

## 類似のライブラリ

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

### react-async

<https://github.com/async-library/react-async>

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

### react-async-hook

<https://github.com/slorber/react-async-hook>

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

## おわりに

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

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