はじめに
RTKQueryのドキュメントを読んで勉強になったので概要、機能についてまとめました。
半分メモ的に書いているので、わかりにくい箇所があるかも知れませんがご了承ください。
RTKQueryとは?
Redux Toolkitに含まれるAdd Onです。データフェッチおよびキャッシュツールで、自分でロジックを書く必要がないです。
Webアプリケーションの多くは、サーバーからデータをフェッチして、データを表示させます。また、そのデータを更新し、更新したデータをサーバーに送信し、クライアント上のキャッシュされたデータをサーバー上のデータと同期させます。
上記のような実装をするときに、以下のようなことを考えロジックを実装する必要がありました。
- 同じデータに対する重複リクエストの回避
- UIをより速く感じさせるため非同期の通信処理
- キャッシュの有効期間の管理
またReactコミュニティでは、「データの取得とキャッシュ」は「状態管理」とは異なると考えました。Reduxのような状態管理ライブラリを使用して、データをキャッシュすることもできますが、ユースケースが異なるため、データフェッチのユースケース専用に構築されたのが、RTKQueryです。
RTKQueryを使うときは?
- 既存のデータ取得ロジックを簡素化したい
- 時間の経過に伴う状態の変更履歴を確認できるようにしたい
- 他のReduxと統合したい
RTKQueryの使い方とは?
これから基本的なRTKQueryの使い方について説明します。
データを取得するカスタムフックを作成
createApi
を使って、どのAPIからどんなデータを取得するのかを設定する。
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
type hogeType = {
id: string,
name: string
}
export const hogeApi = createApi({
reducerPath: 'hogeApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://hogefuga/api/' }),
endpoints: (builder) => ({
getHogeName: builder.query<hogeType, string>({
query: (name) => `hoge/${name}`,
}),
}),
})
export const { useGetHogeByNameQuery } = hogeApi
storeを作成
キャッシュを管理してくれるように、storeにreducerと、middlewareを追加する
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { hogeApi } from './services/hoge'
export const store = configureStore({
reducer: {
[hogeApi.reducerPath]: hogeApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(hogeApi.middleware),
})
setupListeners(store.dispatch)
App.tsx
などでstoreをReduxのProviderに設定
import * as React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './App'
import { store } from './app/store'
const rootElement = document.getElementById('root')
render(
<Provider store={store}>
<App />
</Provider>,
rootElement
)
コンポーネントでQueryを使用する
以下が、APIからのデータのフェッチ方法でした。
import * as React from 'react'
import { useGetHogeByNameQuery } from './services/hoge'
export default function App() {
const { data, error, isLoading } = useGetHogeByNameQuery('hogefuga')
return (
<div className="App">
{error ? (
<>Oh no, there was an error</>
) : isLoading ? (
<>Loading...</>
) : data ? (
<>
<p>{data.id}</p>
<h1>{data.name}</h1>
</>
) : null}
</div>
)
}
キャッシュ動作について
データがサーバーからフェッチされると、RTK クエリはデータを Redux ストアに「キャッシュ」として保存します。同じデータに対して追加のリクエストが実行されると、RTK クエリはサーバーに追加のリクエストを送信するのではなく、既存のキャッシュされたデータを提供します。
queryCacheKey
が実行されると、エンドポイントで使用されるパラメーターがシリアル化され、要求用として内部的に保存されます。同じリクエストを実行する2つのコンポーネントは、キャッシュされた同じデータを使用します。
import { useGetUserQuery } from './api.ts'
const UserProfile1() {
const { data } = useGetUserQuery(1)
return <div>...</div>
}
const UserProfile2() {
const { data } = useGetUserQuery(2)
return <div>...</div>
}
const UserProfile3() {
const { data } = useGetUserQuery(2)
return <div>...</div>
}
上記のようなコンポーネントがあり、すべてがマウントされた場合、UserProfile2とUserProfile3では同じエンドポイントで同じクエリパラメータのため、どちらかではキャッシュが利用されます。
サブスクリプションが削除されると(デフォルトは60秒後)、データはキャッシュから削除されます。有効期限は、 API定義全体keepUnusedDataFor
のプロパティを使用して構成することも、エンドポイントごとに構成することもできます。
refetchする方法
refetchを行った場合、キャッシュを無効にしてデータ取得を行います。
keepUnusedDataFor
の値を秒単位で指定すると、60秒後(デフォルト)に削除されるサブスクリプションを指定することができます。
import { useDispatch } from 'react-redux'
import { useGetPostsQuery } from './api'
const Component = () => {
const dispatch = useDispatch()
const { data, refetch } = useGetPostsQuery({ count: 5 })
function handleRefetchOne() {
// force re-fetches the data
refetch()
}
function handleRefetchTwo() {
// has the same effect as `refetch` for the associated query
dispatch(
api.endpoints.getPosts.initiate(
{ count: 5 },
{ subscribe: false, forceRefetch: true }
)
)
}
return (
<div>
<button onClick={handleRefetchOne}>Force re-fetch 1</button>
<button onClick={handleRefetchTwo}>Force re-fetch 2</button>
</div>
)
}
頻繁に再fetchする場合
refetchOnMountOrArgChange
を使うと、デフォルトの動作よりもfetchを多くしたいときに使用する。
デフォルトでは、refetchOnMountOrArgChange
がfalse
になっています。
true
にすると、クエリに新しいサブスクライバーが追加されたときに、エンドポイントが常に再fetchします。
またnumver
も指定することが可能です。
number
を指定すると、最後に実行されたクエリから指定された秒数が経過すると、常に再fetchされます。
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnMountOrArgChange: 30,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
}),
}),
})
import { useGetPostsQuery } from './api'
const Component = () => {
const { data } = useGetPostsQuery(
{ count: 5 },
// this overrules the api definition setting,
// forcing the query to always fetch when this component is mounted
{ refetchOnMountOrArgChange: true }
)
return <div>...</div>
}
ウィンドウがフォーカスした際に、再fetchする
refetchOnFocus
を使用すると、ウィンドウがフォーカスされた際に、サブスクライブされたすべてのクエリを再fetchします。
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnFocus: true,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
}),
}),
})
setupListeners
も必ず、呼び出す必要があります。
これで、storeの設定を変更します。
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { api } from './services/api'
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (gDM) => gDM().concat(api.middleware),
})
// enable listener behavior for the store
setupListeners(store.dispatch)
export type RootState = ReturnType<typeof store.getState>
ネットワークが復活した際
refetchOnReconnect
は、ネットワークが復活した際に、サブスクライブされた全てのクエリを再fetchします。
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnReconnect: true,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
}),
}),
})
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { api } from './services/api'
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (gDM) => gDM().concat(api.middleware),
})
// enable listener behavior for the store
setupListeners(store.dispatch)
export type RootState = ReturnType<typeof store.getState>
まとめ
Reduxのオプションを使うと、キャッシュ周りでいろいろカスタムができそうでした。
採用のお知らせ
株式会社Relicでは、フロントエンドエンジニアを積極的に採用中です。
またRelicでは、地方拠点がありますので、U・Iターンも大歓迎です!🙌
少しでもご興味がある方は、Relic採用サイトからエントリーください!
参考