10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-08-25

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/loading-forever.gif
(出典: 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>

スクリーンショット 2022-08-24 14.48.12.png

テスト

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の変更前の状態保持など、可能性感じる機能が多いので、ユースケースを集めていきたいところです。

参考

10
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?