React Query(v4)使ってみたので、使い方をまとめてみた


# TL;DR

- VercelのSWRとReduxをまとめたような機能を持つライブラリー
- fetchのコントロール、データの整形、オフライン対応等が含まれている
- 強力なdevtoolあり

# 概要

[公式ドキュメント](https://tanstack.com/query/v4/docs/overview)

[チュートリアル](https://tkdodo.eu/blog/practical-react-query)

作成物：[レポジトリ](https://github.com/rowaxl/react-query-tutorial)

上記チュートリアルの題材に沿って、基本的なTODOアプリを作ってみたので、使い方と特徴をまとめたく思います。

## Installation

```bash
$ npm install @tanstack/react-query
$ yarn add @tanstack/react-query
```

アプリケーショントップで、プロバイダーを用意

```jsx
// index.tsx

import {
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'

// Create a client
const queryClient = new QueryClient()

...
<QueryClientProvider client={queryClient}>
  <App />
</QueryClientProvider>
```

## useQuery

fetchするための基本的なメソッド

基本的なHookの書き方

```jsx
const data = useQuery(queryKeys, queryFn, options)
```

wrapperのカスタムhookを用意することで、クエリのデータ取得、キーや型の整理などが簡単にできる

例）

```jsx
// Todo/hooks/index.ts

export const useTodoQuery = ({ filter, enabled, onError }) => 
  useQuery<Todo[], Error>(
    ['todos', filter],
    () => fetcher(filter),
    {
      notifyOnChangeProps: 'all',
      enabled,
      onError,
    })

// Todo/components/A.tsx
const [filter, setFilter] = useState('all')
const [needFetch, setNeedFetch] = useState(false)
const { data: todo } = useTodoQuery({ 
  filter,              // setFilterで値が変わると、fetchし直す
  enabled: needFetch,  // needFetchがtrueにならないと、クエリを実行しない
  onError: () => console.error('error!')
})

// Todo/components/B.tsx
const { data: todo } = useTodoQuery({ ... })
```

- queryKeys
  - データ取得のための**ユニーク**なキー
  - 更新時にqueryFnを実行
- queryFn
  - `fetch` / `axios`などを実行する
  - 引数が必要な場合  `useQuery([], () => fetcher(args))`
- options
  - 実行時のオプションたち
  - queryFnの実行を制御する`enabled`など
- data(戻り値)
  - クエリ結果、エラー、状態など

### **queryKeys: unknown[]**

取得したデータに紐づかせるための、**ユニークなキー**
`useQuery(['todo', 'detail', 1])`のように可読性重視のパターンも可(todoの詳細、idは1みたいな)
配列でハッシュされるので、配列内のオブジェクトの変更ではrevalidateしない

### **queryFn**

通信用のfunction
引数が必要な場合: `useQuery(['todos', state, sorting], () => queryFn(state, sorting))`

queryFnには引数として`QueryFunctionContext`　が渡されるので、引数が複数必要な場合、以下のようなまとめ方も可

```jsx
const fetchTodos = async ({ queryKey }) => {
  const [, state, sorting] = queryKey // 第一引数は'todo'なのでこの場合不要
  const response = await axios.get(`todos/${state}?sorting=${sorting}`)
  return response.data
}

...

useQuery(['todos', state, sorting], fetchTodos)
```

### **options**

`queryFn` 実行時のオプション
デフォルトは、プロバイダーに渡す時に、`new QueryClient({ defaultOptions: {...} })`と設定

**enabled?: boolean**

`queryFn`　の実行判定

活用ケース

- クエリに依存関係があるとき
    - e.g. ページのdetailを取得してから、それに紐づくcommentsを取得する
- クエリのオンオフ
    - e.g. モーダルが開いていると、バックグラウンドでクエリさせないなど
- ユーザーインプットの待ち
    - ドラフトなど、特定のユーザーインプット後のクエリを実行させない

**select?: function**

`queryFn`の結果を引数でもらって、transformしてdataとして戻せるfunction
`data`が存在するときだけ呼ばれる

e.g. 

```jsx
const useCountTodo = () => 
  useQuery(['todos'], fetchTodos, {
	select: (data) => data.length,
    initialData: [] // ※dataの型
})

const count = useCountTodo() // 初期値は0、dataが更新されると、data.lengthの値
```

**staleTime?: number**

マウント後、fetchしたデータがいつ破棄されるかを決める値

fetch実行前に返される`initialData`の破棄にも影響

※ staleTimeのdefaultが`0`のため、initialDataでのマウント直後に再度fetchが発生。

クエリキャッシュなどから使うなど、ポリシーに沿って再取得オプションの設定に必要

**notifyOnChangeProps?: ‘all’ | unknown[]**

useEffectにおけるdependencyのように、クエリ内で変化を監視したいものを設定

`'all'`の場合は全て、一部のみの場合は`['data', 'error']`のように渡す

詳細は以下のData（戻り値）を参照

### Data（戻り値）

戻り値は、以下のような構造となる

dataはstateとして内部で管理されるため、**最新の値を維持するためには、*ローカルstateにコピーしないことが推奨されている***
  

```jsx
{
  data?   // クエリの結果
  error?  // クエリ取得中にエラーが発生した場合
  ...
  status: 'loading' | 'error' | 'success' | ~~'idle'~~ // v4で削除
  fetchStatus: 'fetching' | 'paused' | 'idle'
  isLoading: boolean
  isFetching: boolean
  isSuccess: boolean
  ... // 状態関連項目にまとめ
  refetch: function
}
```

**状態管理**

statusは、以下の状態を表示します

`success`: クエリ実行に成功し、`data`に結果が入っている

`error`: クエリ実行に失敗し、`error` 内にエラーが入っている

`loading`: クエリにデータはなく、現在取得中であること

fetchStatusは、よりネットワーク状態の方にフォーカスしている

`idle`: クエリは何もしていない状態

`fetching`: クエリが実行中

`paused`: **ネットワークがオフラインのため**、クエリを止めている状態

![https://tkdodo.eu/blog/loading-forever.gif](https://tkdodo.eu/blog/loading-forever.gif)
(出典: [https://tkdodo.eu/blog/offline-react-query](https://tkdodo.eu/blog/offline-react-query))


**refetch**

*クエリキーを変えずに*、再度fetchを行う

引数の更新が必要な場合など、fetch内容を変えたい場合は`refetch`を使えないため注意

```jsx
const [id, setId] = useState(1)

const { data, refetch } = useQuery(['todos', id], () => fetchTodo({ id }))

<button onClick={() => {
  // 🚨 id: 1での再度fetchするだけ
  refetch({ id: 2 })
  refetch()

  // ✅ id: 2でfetchを行う
  setId(2)
}})>Show Todo(2)</button>
```

## useMutation

POST / PUT / DELETE等のリクエスト実行する用

```jsx
const useAddTodo = () => {
  const queryClient = useQueryClient()

  return useMutation(
    (newTodo) => fetch(URL, { body: { todo: newTodo } }),
    options
  )
}

...
const addTodo = useAddTodo()

<button onClick={() => addTodo.mutate(newTodo)}>
Add
</button>
```

**※ mutateメソッドは1つの引数しか渡せない**

### mutate / mutateAsync

`mutate`: voidを返す

`mutateAsync`: Promiseを返す

### options

**onMutate / onSuccess / onError / onSettled**
mutationの開始〜終了まで呼ばれるコールバック

- `onMutate: (variables)`
    - `.mutate()` がコールされ、mutation実行される前に呼ばれる
    - Optimistic Updates時、あらかじめqueryをアップデートして再レンダーを促すなどできる
    - 戻り値はcontextに格納されるので、mutation実行前の値などを格納できる
- `onSuccess: (data, variables, context) => {}`
    - クエリの実行に成功した時に呼ばれるコールバック
    - **mutateではクエリ内のdataはアップデートされない**ので、`queryClient.invalidateQuery([’todos’])`　でクエリを再取得するか、`queryClient.setQueryData(['todos'], (old) => [...old, newTodo])` でクエリを直接アップデートすることで、レンダーをトリガー
    - `invalidateQueries()`をreturnするとinvalidationが終わるまで待つ。returnしない場合は、awaitしない
- `onError: (err, variable, context) => {}`
    - mutation中にエラー発生時
    - Optimistic Updates時、ロールバックする
- `onSettled: (data, error, variables, context) => {}`
    - onSuccess / onError実行後に呼ばれる
    - `invalidateQuery`してクエリを最新状態にするなど

**retry: number**

mutationのリトライ回数

オフラインでmutationが実行されなかった場合、再接続後にリトライを行う

## Typescript

v4におけるuseQueryのジェネリック定義

```jsx
const { data, error } = useQuery<
  TQueryFnData = unknown,
  TError = unknown,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey
>
```

`TQueryFnData`: queryFnの戻り値の型

`TError`: `error`の型

`TData`: selectで整形した場合の型

`TQueryKey`: デフォルトで `unknown[]`。useQueryの第一引数 → [バリデーションの例](https://tkdodo.eu/blog/react-query-and-type-script#typing-the-default-query-function)

## Dev tools

クエリのstate、状態等が確認したいとき

デフォルトだと、**`process.env.NODE_ENV === 'development'`のときだけ、画面左下にフロートで表示される**

オプションの詳細等は[こちら](https://tanstack.com/query/v4/docs/devtools)

```
$ npm i @tanstack/react-query-devtools
$ yarn add @tanstack/react-query-devtools
```

```jsx
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

...
<QueryClientProvider client={queryClient}>
  <App />
  <ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
```

![スクリーンショット 2022-08-24 14.48.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345464/6df7bbea-c842-827b-f4d1-f42bf91fe18d.png)


## テスト

Hookのテストには、`@testing-library/react-hooks` を使用

fetch部分には [Mock Service Worker](https://mswjs.io/)でモックする

### Installation

```bash
$ npm install @testing-library/react-hooks react-test-renderer --save-dev
$ yarn add --dev @testing-library/react-hooks react-test-renderer
```

### Providerの生成

```jsx
import {
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'

const createWrapper = () => {
  const queryClient = new QueryClient()
  return ({ children }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  )
}
```

### カスタムhookのテスト

```jsx
import { renderHook } from '@testing-library/react-hooks'

test("my first test", async () => {
  const useCustomHook = () => useQuery(['customHook'], () => 'Hello')

  const { result } = renderHook(() => useCustomHook(), {
    wrapper: createWrapper()
  })
})
```

# 感想

`swr` の条件付きfetchやmutateを使いやすくまとめ上げ、なお`redux`のようなGlobal State(react-queryではServer State / Client Stateという言い方をしていました)の最新の状態にする、オン・オフラインで管理する、そしてそれをなるべく簡単に書く、といったかゆいとこに手が届く感じのライブラリでした。

直接`swr`と競争すると思いますので、導入段階で脱reduxしたいようでしたらお試しいただきたいところです。

オフラインクエリや、Stateの変更前の状態保持など、可能性感じる機能が多いので、ユースケースを集めていきたいところです。

# 参考

- [TanStack Query(v4)](https://tanstack.com/query/v4/docs/overview)
- [Practical React Query by TkDodo](https://tkdodo.eu/blog/practical-react-query)