キャッシュ動作
RTK Queryの重要な機能の1つは、キャッシュされたデータの管理です。サーバーからデータを取得すると、RTK QueryはデータをReduxストアに「キャッシュ」として保存します。同じデータに対する追加の要求が行われた場合、RTK Queryはサーバーに追加の要求を送信するのではなく、既存のキャッシュされたデータを提供します。
RTK Queryは、キャッシュ動作を操作し、ニーズに合わせて調整するための多くの概念とツールを提供します。
デフォルトのキャッシュ動作
RTK Queryでは、キャッシングは次を基にしています。
- APIエンドポイントの定義
- コンポーネントがエンドポイントからデータにサブスクライブするときに使用されるシリアライズされたクエリパラメータ
- アクティブなサブスクリプションの参照カウント
サブスクリプションが開始されると、エンドポイントで使用されるパラメータがシリアライズされ、要求のqueryCacheKey
として内部的に保存されます。同じqueryCacheKey
を生成する将来の要求(つまり、シリアライズを考慮した同じパラメータで呼び出される)は、元の要求に対して重複排除され、同じデータと更新を共有します。つまり、同じ要求を実行する2つの異なるコンポーネントは、同じキャッシュされたデータを使用します。
要求が試行されると、データが既にキャッシュに存在する場合は、そのデータが提供され、サーバーに新しい要求は送信されません。そうでない場合、データがキャッシュに存在しない場合は、新しい要求が送信され、返されたレスポンスがキャッシュに保存されます。
サブスクリプションは参照カウントされます。同じエンドポイントとパラメータを要求する追加のサブスクリプションは、参照カウントを増やします。データへのアクティブな「サブスクリプション」がある限り(例:エンドポイントのuseQuery
フックを呼び出すコンポーネントがマウントされている場合)、データはキャッシュに残ります。サブスクリプションが削除されると(例:データに最後にサブスクライブしたコンポーネントがアンマウントされると)、一定時間(デフォルトは60秒)後に、データはキャッシュから削除されます。有効期限は、API定義全体とエンドポイントごとのkeepUnusedDataFor
プロパティで設定できます。
キャッシュの有効期間とサブスクリプションの例
クエリパラメータとしてid
を期待するエンドポイントと、この同じエンドポイントからデータ要求している4つのマウントされたコンポーネントがあるとします。
import { useGetUserQuery } from './api.ts'
function ComponentOne() {
// component subscribes to the data
const { data } = useGetUserQuery(1)
return <div>...</div>
}
function ComponentTwo() {
// component subscribes to the data
const { data } = useGetUserQuery(2)
return <div>...</div>
}
function ComponentThree() {
// component subscribes to the data
const { data } = useGetUserQuery(3)
return <div>...</div>
}
function ComponentFour() {
// component subscribes to the *same* data as ComponentThree,
// as it has the same query parameters
const { data } = useGetUserQuery(3)
return <div>...</div>
}
4つのコンポーネントがエンドポイントにサブスクライブしている間、エンドポイントとクエリパラメータの組み合わせは3つだけです。クエリパラメータ1
と2
はそれぞれ1つのサブスクライバーを持ち、クエリパラメータ3
は2つのサブスクライバーを持ちます。RTK Queryは3つの異なるフェッチを実行します。エンドポイントごとに一意のクエリパラメータセットごとに1つずつ。
データは、少なくとも1つのアクティブなサブスクライバーがそのエンドポイントとパラメータの組み合わせに関心を持っている限り、キャッシュに保持されます。サブスクライバーの参照カウントがゼロになると、タイマーが設定され、タイマーが期限切れになるまでにそのデータへの新しいサブスクリプションがない場合、キャッシュされたデータは削除されます。デフォルトの有効期限は60秒で、API定義全体とエンドポイントごとに設定できます。
上記の例で「ComponentThree」がアンマウントされた場合、経過時間にかかわらず、「ComponentFour」がまだ同じデータにサブスクライブしており、サブスクライブ参照カウントが1
であるため、データはキャッシュに残ります。「ComponentFour」がアンマウントされると、サブスクライバーの参照カウントは0
になります。データは有効期限の残り時間の間、キャッシュに残ります。タイマーが期限切れになる前に新しいサブスクリプションが作成されていない場合、キャッシュされたデータは最終的に削除されます。
キャッシュ動作の操作
デフォルトの動作に加えて、RTK Queryは、無効と見なされるシナリオや、そうでなければ「更新」するのに適しているとみなされるシナリオで、データをより早く再フェッチするための多くのメソッドを提供します。
keepUnusedDataFor
を使用したサブスクリプション時間の削減
上記デフォルトのキャッシュ動作とキャッシュの有効期間とサブスクリプションの例で説明したように、デフォルトでは、サブスクライバーの参照カウントがゼロになった後、データは60秒間キャッシュに残ります。
この値は、API定義とエンドポイントごとにkeepUnusedDataFor
オプションを使用して設定できます。エンドポイントごとのバージョンが提供されている場合、API定義の設定よりも優先されます。
秒単位の数値としてkeepUnusedDataFor
に値を指定すると、サブスクライバーの参照カウントがゼロになった後、データがキャッシュに保持される時間を指定できます。
- TypeScript
- JavaScript
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
keepUnusedDataFor: 30,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
// configuration for an individual endpoint, overriding the api setting
keepUnusedDataFor: 5,
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
keepUnusedDataFor: 30,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => `posts`,
// configuration for an individual endpoint, overriding the api setting
keepUnusedDataFor: 5,
}),
}),
})
refetch
/initiate
を使用したオンデマンドでの再フェッチ
useQuery
またはuseQuerySubscription
フックから結果プロパティとして返されるrefetch
関数を使用して、データの再フェッチに対する完全な粒度制御を実現できます。
refetch
関数を呼び出すと、関連付けられたクエリが強制的に再フェッチされます。
あるいは、エンドポイントのinitiate
サンクアクションをディスパッチし、同じ効果を得るためにサンクアクションクリエイターにforceRefetch: true
オプションを渡すこともできます。
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>
)
}
refetchOnMountOrArgChange
を使用した再フェッチの促進
refetchOnMountOrArgChange
プロパティを使用して、通常よりも頻繁にクエリが再フェッチされるように促すことができます。これは、エンドポイント全体、個々のフック呼び出し、またはinitiate
アクションのディスパッチ時に渡すことができます(アクションクリエイターのオプションの名前はforceRefetch
です)。
refetchOnMountOrArgChange
は、デフォルトの動作でキャッシュされたデータが提供される追加の状況で再フェッチを促すために使用されます。
refetchOnMountOrArgChange
は、ブール値または秒単位の数値を受け入れます。
このプロパティにfalse
(デフォルト値)を渡すと、上記で説明したデフォルトの動作が使用されます。
このプロパティにtrue
を渡すと、クエリの新しいサブスクライバーが追加されたときに、エンドポイントが常に再フェッチされるようになります。API定義自体ではなく、個々のフック呼び出しに渡された場合、これはそのフック呼び出しのみに適用されます。つまり、フックを呼び出すコンポーネントがマウントされるとき、または引数が変更されるとき、エンドポイントと引数の組み合わせのキャッシュされたデータが既に存在するかに関係なく、常に再フェッチされます。
秒単位の数値を値として渡すと、次の動作が使用されます。
- クエリサブスクリプションが作成された時点では
- キャッシュに既存のクエリがある場合、現在の時間と、そのクエリの最後に実行されたタイムスタンプを比較します。
- 指定された秒数が経過している場合は再フェッチします。
- クエリがない場合は、データをフェッチします。
- 既存のクエリがあるが、最後のクエリ以降に指定された時間が経過していない場合は、既存のキャッシュされたデータを提供します。
- TypeScript
- JavaScript
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 { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnMountOrArgChange: 30,
endpoints: (builder) => ({
getPosts: builder.query({
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>
}
refetchOnFocus
を使用したウィンドウフォーカス時の再フェッチ
refetchOnFocus
オプションを使用すると、アプリケーションウィンドウがフォーカスを取り戻した後に、RTK Queryがサブスクライブされたすべてのクエリを再フェッチしようとするかどうかを制御できます。
このオプションをskip: true
と共に指定した場合、skip
がfalseになるまで評価されません。
これには、setupListeners
が呼び出されている必要があります。
このオプションは、createApi
でのAPI定義と、useQuery
、useQuerySubscription
、useLazyQuery
、useLazyQuerySubscription
フックの両方で使用できます。
- TypeScript
- JavaScript
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`,
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnFocus: true,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => `posts`,
}),
}),
})
- TypeScript
- JavaScript
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>
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)
refetchOnReconnect
を使用したネットワーク再接続時の再フェッチ
createApi
のrefetchOnReconnect
オプションを使用すると、ネットワーク接続を回復した後に、RTK Queryがサブスクライブされたすべてのクエリを再フェッチしようとするかどうかを制御できます。
このオプションをskip: true
と共に指定した場合、skip
がfalseになるまで**評価されません**。
これには、setupListeners
が呼び出されている必要があります。
このオプションは、createApi
でのAPI定義と、useQuery
、useQuerySubscription
、useLazyQuery
、useLazyQuerySubscription
フックの両方で使用できます。
- TypeScript
- JavaScript
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 { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnReconnect: true,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => `posts`,
}),
}),
})
- TypeScript
- JavaScript
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>
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)
キャッシュタグの無効化によるミューテーション後の再フェッチ
RTK Queryは、ミューテーションエンドポイントの影響を受けるデータを持つクエリエンドポイントの再フェッチを自動化するためのオプションのキャッシュタグシステムを使用します。
この概念の詳細については、自動再フェッチを参照してください。
トレードオフ
正規化または重複排除されたキャッシュなし
RTK Queryは、意図的に**複数の要求にわたって同一のアイテムを重複排除するキャッシュを実装していません**。これにはいくつかの理由があります。
- 完全に正規化されたクエリ間で共有されるキャッシュは、解決が難しい問題です。
- 現在、その問題を解決するための時間、リソース、関心がありません。
- 多くの場合、データが無効になったときに単純にデータを再フェッチする方が効果的で、理解しやすいです。
- 少なくとも、RTKQは「いくつかのデータを取得する」という一般的なユースケースを解決するのに役立ち、多くの人にとって大きな痛点です。
例として、getTodos
とgetTodo
エンドポイントを持つAPIスライスがあり、コンポーネントが次のクエリを実行しているとします。
getTodos()
getTodos({filter: 'odd'})
getTodo({id: 1})
これらのクエリ結果はそれぞれ、{id: 1}
のようなTodoオブジェクトを含みます。
完全に正規化された重複排除キャッシュでは、このTodoオブジェクトのコピーは1つしか保存されません。しかし、RTK Queryは各クエリ結果をキャッシュに個別に保存します。そのため、このTodoの3つの別々のコピーがReduxストアにキャッシュされます。ただし、すべてのエンドポイントが({type: 'Todo', id: 1}
など)一貫して同じタグを提供している場合、そのタグの無効化は、一貫性を保つためにすべての該当するエンドポイントにデータの再取得を強制します。
Reduxのドキュメントでは常に、IDによるアイテムの容易な検索とストアでの更新を可能にするためにデータを正規化されたルックアップテーブルに保持することが推奨されており、RTKのcreateEntityAdapter
は正規化された状態の管理を支援するために設計されました。これらの概念は依然として価値があり、なくなることはありません。ただし、RTK Queryを使用してキャッシュデータを管理している場合、自分でそのようにデータを操作する必要性は少なくなります。
さらに役立つ点がいくつかあります。
- 生成されたクエリフックには、
selectFromResult
オプションがあり、コンポーネントはクエリ結果から個々のデータを読み取ることができます。例として、<TodoList>
コンポーネントはuseTodosQuery()
を呼び出し、各<TodoListItem>
は同じクエリフックを使用して結果から選択し、適切なTodoオブジェクトを取得できます。 transformResponse
エンドポイントオプションを使用して、取得したデータを変更し、異なる形状で保存することができます。たとえば、createEntityAdapter
を使用して、キャッシュに挿入する前に、この1つのレスポンスに対してデータを正規化することができます。
詳細情報
例
キャッシュサブスクリプションのライフタイムデモ
この例は、サブスクライバーの参照カウントとkeepUnusedDataFor
の値がどのように相互作用するかを示すライブデモです。デモでは、Subscriptions
とQueries
(キャッシュされたデータを含む)が表示され、視覚化できます(これはRedux Devtools Extensionでも確認できます)。
2つのコンポーネントがマウントされ、それぞれ同じエンドポイントクエリ(useGetUsersQuery(2)
)を使用します。コンポーネントのオン/オフを切り替える際に、サブスクライバーの参照カウントが減少することを確認できます。両方のコンポーネントをオフにしてサブスクライバーの参照カウントがゼロになると、Queries
セクションのキャッシュされたデータは5秒間(このデモのエンドポイントに指定されたkeepUnusedDataFor
の値)保持されます。サブスクライバーの参照カウントが完全にゼロのままである場合、キャッシュされたデータはストアから削除されます。