TL;DR
- VercelのSWRとReduxをまとめたような機能を持つライブラリー
- fetchのコントロール、データの整形、オフライン対応等が含まれている
- 強力なdevtoolあり
概要
作成物:レポジトリ
上記チュートリアルの題材に沿って、基本的なTODOアプリを作ってみたので、使い方と特徴をまとめたく思います。
Installation
$ npm install @tanstack/react-query
$ yarn add @tanstack/react-query
アプリケーショントップで、プロバイダーを用意
// index.tsx
import {
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
// Create a client
const queryClient = new QueryClient()
...
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
useQuery
fetchするための基本的なメソッド
基本的なHookの書き方
const data = useQuery(queryKeys, queryFn, options)
wrapperのカスタムhookを用意することで、クエリのデータ取得、キーや型の整理などが簡単にできる
例)
// 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
が渡されるので、引数が複数必要な場合、以下のようなまとめ方も可
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.
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にコピーしないことが推奨されている
{
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/offline-react-query)
refetch
クエリキーを変えずに、再度fetchを行う
引数の更新が必要な場合など、fetch内容を変えたい場合はrefetch
を使えないため注意
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等のリクエスト実行する用
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のジェネリック定義
const { data, error } = useQuery<
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey
>
TQueryFnData
: queryFnの戻り値の型
TError
: error
の型
TData
: selectで整形した場合の型
TQueryKey
: デフォルトで unknown[]
。useQueryの第一引数 → バリデーションの例
Dev tools
クエリのstate、状態等が確認したいとき
デフォルトだと、process.env.NODE_ENV === 'development'
のときだけ、画面左下にフロートで表示される
オプションの詳細等はこちら
$ npm i @tanstack/react-query-devtools
$ yarn add @tanstack/react-query-devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
...
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
テスト
Hookのテストには、@testing-library/react-hooks
を使用
fetch部分には Mock Service Workerでモックする
Installation
$ npm install @testing-library/react-hooks react-test-renderer --save-dev
$ yarn add --dev @testing-library/react-hooks react-test-renderer
Providerの生成
import {
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
const createWrapper = () => {
const queryClient = new QueryClient()
return ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
カスタムhookのテスト
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の変更前の状態保持など、可能性感じる機能が多いので、ユースケースを集めていきたいところです。