【React】React Queryの基礎知識をまとめました(Stale, Cache, Prefetchingなど)

# はじめに
新しい現場ではReactの状態管理にReduxを使っているのですが、Action CreatorやReducerなど、コード量の多さに辟易してしまうことがしばしばあります。
そんな悩みを解決する状態管理ライブラリとしてReact Queryが流行っているらしいので、このたび勉強してみることにしました。

以前書いた状態管理の記事もあわせてご覧いただければ幸いです。

https://qiita.com/suzuki0430/items/61317b772e7d4d480b80

# React Queryとは
React Queryの大きな特徴として、サーバから取得したデータをクライアントでキャッシュとして保管できることが挙げられます。
また、新しいデータを取得したときにいつキャッシュを更新するか、なども管理することができます。

# 初期導入
`npm install react-query`でパッケージをインストールした後、クエリとキャッシュを管理するための`queryClient`を作成し、Rootのコンポーネントを`QueryClientProvider`でラップします。
`QueryClientProvider`はキャッシュやクライアント設定を下のコンポーネントに与えたり、値として`queryClient`を取得する役割をもちます。

あとはラップされたコンポーネント内で`useQuery()`というHooksを使うことで、クエリを行うことができます。

```jsx:App.jsx
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <div className="App">
        <Posts />
      </div>
      <ReactQueryDevtools />
    </QueryClientProvider>
  );
}
```

ラップされたコンポーネントの中に`ReactQueryDevTool`というものがありますが、これはReact Queryで管理しているデータの変化を確認するための開発者用ツールです。
![スクリーンショット 2021-12-02 21.52.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/134a4860-2a5f-e797-9297-df51a01b170d.png)

# Fetching
以下のようなデータをフェッチする関数から`useQuery()`でデータを取得してみます。

```js
async function fetchPosts(pageNum) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts?_limit=10&_page=${pageNum}`
  );
  return response.json();
}
```
`useQuery()`の第一引数には`key`を、第二引数にはフェッチ関数をコールバックとして配置します。
すると、`data`をはじめとした様々なプロパティを取得することができます。

```js
  const { data, isError, error, isLoading, isFetching } = useQuery(
    'posts',
    () => fetchPosts(currentPage)
  );
```
## isFetchingとisLoadingの違い
`isFetching`はクエリ関数がデータをフェッチするまでの状態のことをいい、`isLoading`は`isFetching`の状態に加えてキャッシュデータももっていない状態のことをいいます。
つまり、キャッシュをもっていれば`isLoading`は`false`となり、`isFetching ⊃ isLoading`といった関係になります。

例えば、訪問済みのページに再度訪れたとき、キャッシュがあるのでデータはちゃんと表示されていますが、下の`ReactQueryDevTools`の画面をみると`fetching`という状態になっていることがわかります。
![スクリーンショット 2021-12-02 22.26.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/f9d47027-6f09-9064-f8ae-19613c60d19e.png)

## Stale timeとCache timeの違い
以上の画面で、`fetching`の右側に`stale`とありますが、これもReact Queryのキャッシュ機構を理解する上で大事なキーワードとなります。

Stale timeというのは、キャッシュデータが古くなったとみなす時間のことをいいます。
Stale timeはデフォルトで`0 s`なのですが、以下のように`staleTime: 1000`などと設定することで、`1000 ms`以内に再訪問したページであればデータを新しいもの(fresh)とみなし、フェッチを行わずにキャッシュを利用するようになります。
`1000 ms`を超えた場合にはキャッシュが使えなくなり、データを再フェッチします。

```js
  const { data, isError, error, isLoading, isFetching } = useQuery(
    'posts',
    () => fetchPosts(currentPage),
    {
      staleTime: 1000,
    }
  );
```

一方、Cache timeはデータをキャッシュする時間のことをいいます。
デフォルトの設定は5分(300000 ms)となっています。
`staleTime: 0` `cacheTime: 300000`のとき、同じページを再訪問するとキャッシュデータが画面に表示されますが、`staleTime`の時間が過ぎてキャッシュデータは古いものとみなされるため、バックグラウンドで再フェッチが実行されます。

## useQuery()のkey
`useQuery()`の第一引数に単一の`key`を与えるとき、再フェッチのトリガーとなるのは以下のようなケースです。

- コンポーネントの再マウント
- ウィンドウの再フォーカス
- 再フェッチ関数の実行

これらのケースから外れてしまうと、クエリが実行されず再フェッチも行われません。
この問題を解決するためには、`key`を依存配列として扱い、中身の値が変わったときに`useQuery()`を実行するようにします。

例えば、ページネーションで`currentPage`が変わったときに`useQuery()`を実行させるとしたら、以下のような記述となります。

```js
  const { data, isError, error, isLoading, isFetching } = useQuery(
    ['posts', currentPage],
    () => fetchPosts(currentPage),
    {
      staleTime: 2000,
    }
  );
```

# Prefetching
Prefetchingとは、予測されるデータをキャッシュに書き込んでおき、ページを開いてフェッチを行っている間、キャッシュデータを表示されるようにすることです。

`useEffect()`に`prefetchQuery()`を仕込んでおくことで、現在のページを開いたときに、次のページのデータを`['posts', nextPage]`の`key`にキャッシュしています。

```js
  const queryClient = useQueryClient();

  useEffect(() => {
    if (currentPage < maxPostPage) {
      const nextPage = currentPage + 1;
      queryClient.prefetchQuery(['posts', nextPage], () =>
        fetchPosts(nextPage)
      );
    }
  }, [currentPage, queryClient]);
```

# Mutation
`useQuery()`ではデータの取得処理を行いましたが、書込処理を行うためのHooksとして`useMutation()`というものもあります。
`useQuery()`との違いとしては以下のものがあります。

- `mutate`関数を返す
- `key`は不要
- `isLoading`はあるが`isFetching`はなし
- デフォルトでリトライなし(クエリはデフォルト3回)

例えば、以下のようなデータ削除のための関数を考えます。

```js
async function deletePost(postId) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/postId/${postId}`,
    { method: 'DELETE' }
  );
  return response.json();
}
```
`useMutation()`から`deleteMutation`を定義します。

```js
const deleteMutation = useMutation((postId) => deletePost(postId));
```
Deleteボタンに仕込むことで、データの削除を実行することができます。

```jsx
<button onClick={() => deleteMutation.mutate(post.id)}>Delete</button>
```

# 参考資料

https://www.udemy.com/share/104FBU3@AIOgCwTt9aiQTgCkwomfmXzQiTtxQTkSrP4ot999ubiWZ2i3wqFPsUItpZ3MJyja/

https://blog.microcms.io/optimize-cache-with-react-query/

https://react-query.tanstack.com/reference/useQuery

https://zenn.dev/brachio_takumi/articles/20210226-react-query