react-queryをsuspenseモードで使って快適な非同期処理をする

## react-queryとは

[react-query HP](https://react-query.tanstack.com/docs/overview)によると
> Simply put, React Query makes fetching, caching, synchronizing and updating server state in your React applications a breeze.

とあります。
触ってみた肌感では、[vercel/swr](https://github.com/vercel/swr)のような感じのhooksと、[apollo client](https://github.com/apollographql/apollo-client)のようなキャッシュ操作が可能な、使いやすいライブラリでした。

実際、公式にも[両ライブラリとの機能の比較](https://react-query.tanstack.com/docs/comparison)が分かりやすい表で示されています。

また、[react-query-devtools](https://github.com/tannerlinsley/react-query-devtools)を入れれば、[Apollo Client Devtools](https://github.com/apollographql/apollo-client-devtools) のような見た目のdevtool上でキャッシュの状態を確認したり、refetchを実行したりすることができる為、非常に快適な開発体験を得られます。

## 使ってみる

以下は公式のexampleです。

```typescript
import React from "react";
import ReactDOM from "react-dom";
import { useQuery } from "react-query";
import { ReactQueryDevtools } from "react-query-devtools";

export default function App() {
  const { isLoading, error, data } = useQuery("repoData", () =>
    fetch(
      "https://api.github.com/repos/tannerlinsley/react-query"
    ).then((res) => res.json())
  );

  if (isLoading) return "Loading...";

  if (error) return "An error has occurred: " + error.message;

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
      <strong>👀 {data.subscribers_count}</strong>{" "}
      <strong>✨ {data.stargazers_count}</strong>{" "}
      <strong>🍴 {data.forks_count}</strong>
      <ReactQueryDevtools initialIsOpen />
    </div>
  );
}

```

`useQuery` の第一引数はユニークなkey（文字列など）で、第二引数はfetcherです。

fetcherとは、非同期な関数で、主にはAPIを叩いたりするような処理が入るでしょう。

第一引数で渡すkeyが何に使われるのかというと、公式によれば
>The unique key you provide is used internally for refetching, caching, deduping related queries.

とあるので、cache周りなどをよしなにやってくれるのに使われるようです。

逆に言えば、ここでランダムな文字列を渡してしまうとキャッシュが効かなくなる、と読めるので、このあたりはApollo Clientに似た仕組みのような感じがします。

`useQuery` の返り値としては、

- `data` (fetcherの返り値をresolveした値）
- `isLoading`（fetcherの返したPromiseが終了したかどうか）
- `error`（fetcherがrejectされた時のError）

などがあります。

`isLoading` を使ってUIを出し分けるパターンは、Apolloなどのライブラリでもおなじみですね。

しかしながら、せっかくの宣言的UIというパラダイムの中において、このような `isLoading` での分岐が存在すると、コンポーネントの複雑さがドンドン増し、また、`data`の返り値も `T | undefined` になってしまうため、型の扱いが面倒です。

そこで登場するのがSuspenseモードです。
（一応experimentalではあるので、自己判断で使いましょう）

## Suspenseモードがあると何が嬉しいのか

Suspenseの仕組みについては、[React公式](https://reactjs.org/docs/concurrent-mode-suspense.html)に説明を譲ります。

react-queryでは、類似ライブラリのvercel/swrと非常によく似たやり方で、Suspenseモードを提供しています。

方法は、

1.グローバルレベルで有効にする

```typescript

 // Configure for all queries
 import { ReactQueryConfigProvider } from 'react-query'
 
 const queryConfig = {
   shared: {
     suspense: true,
   },
 }
 
 function App() {
   return (
     <ReactQueryConfigProvider config={queryConfig}>
       ...
     </ReactQueryConfigProvider>
   )
 }
```

2.クエリレベルで有効にする

```typescript
 import { useQuery } from 'react-query'
 
 // Enable for an individual query
 useQuery(queryKey, queryFn, { suspense: true })
```

の2つがあります。

## SuspenseモードをTypeScriptで使う上で苦労した点

このライブラリを触り始める時に少し期待していたのは、
suspenseモードで使った `useQuery` の返り値はnon nullableになることでした。

Suspenseの仕組み上は、常にdefinedになるはずですが、

```typescript
const { data } = useQuery(queryKey, queryFn, { suspense: true})
// このdataの型は `T | undefined` になってしまう
```

このようにnullableになってしまいます。
（おそらくグローバルレベルでのSuspenseモードの使い方があるせいで型の静的解析が不可能？）

そこで、これは筆者独自のワークアラウンドとして、ラッパーのhooksを作りました。

```typescript:useQueryWithSuspense.ts
import { useQuery, QueryFunction, QueryKey, QueryResult } from "react-query";

type RequireData<T extends { data: unknown }> = T & {
  data: NonNullable<T["data"]>;
};

type UseQueryWithSuspenseResult<T> = RequireData<QueryResult<T, unknown>>;

export const useQueryWithSuspense = <T extends unknown>(queryKey: QueryKey, fetcher: QueryFunction<T>): UseQueryWithSuspenseResult<T> => {
  return useQuery(queryKey, fetcher, { suspense: true }) as any;
};
```

この `useQueryWithSuspense` 関数は内部で `useQuery` を Suspenseモードで使っていて、なおかつ**返り値が常にdefinedになります。**


ただし、筆者はまだ様々なエッジケースを検証できておらず、型のつけ方にも悩んでいるので、もしより良い実装方法があればご意見を歓迎しています！


## まとめ

筆者は今、ある新規プロジェクトで非同期処理・状態管理周りをどうするか考えていました。
当初はReduxを使ってローディング・エラーなどの状態を処理することを検討していましたが、
action creatorやreducerなどの実装・型付け・テストで書かなければいけない行数が非常に多いことが悩みの種でした。

いっそreact-queryとSuspenseという仕組みに全てを委ねた結果、**行数をおおよそ80%ほど縮小することができました。** （※体感値）

当然ながら、アプリケーションの構成や規模によっては、こういう判断ができない場合も沢山ありますが、
もしも、

- Reduxで管理しているのは、APIのリクエスト処理周りばかりだ
- 大量の `isLoading` や、Reactのライフサイクルなどで消耗している

という場合は、一度検討してみることをオススメします！