手動キャッシュ更新
概要
ほとんどの場合、バックエンドの変更をトリガーした後に最新データを受信するには、キャッシュタグの無効化を利用して自動再フェッチを実行できます。この機能を使用すると、データが無効になるようなミューテーションが発生したことが通知されると、クエリによってデータが再フェッチされます。
ほとんどの場合、手動キャッシュ更新よりも自動再フェッチを使用することをお勧めします。
しかし、「楽観的」または「悲観的」更新、またはキャッシュエントリライフサイクルの一環としてのデータ変更など、手動キャッシュ更新が必要なユースケースもあります。
RTK クエリは、これらのユースケース用のサンクをエクスポートし、api.utils
にアタッチします。
updateQueryData
: 既存のキャッシュエントリを更新します。upsertQueryData
: キャッシュエントリを作成または置き換えます。
これらはサンクであるため、dispatch
にアクセスできる場所ならどこからでもディスパッチできます。
既存のキャッシュエントリの更新
既存のキャッシュエントリの更新には、updateQueryData
を使用します。
updateQueryData
は厳密に既存のキャッシュエントリへの更新を実行することを目的としており、新しいエントリを作成することはできません。endpointName
+ args
の組み合わせが既存のキャッシュエントリと一致しないupdateQueryData
サンクアクションがディスパッチされると、提供されたrecipe
コールバックは呼び出されず、patches
またはinversePatches
は返されません。
キャッシュエントリを手動更新するユースケース
- ミューテーションが試行されたときのユーザーへすぐのフィードバックの提供
- ミューテーションの後、リスト全体を取り直すのではなく、すでにキャッシュされているアイテムのリストの単一のアイテムの更新
- すぐにフィードバックを得た大量のミューテーションのデバウンス。これらはまるで適用されているかのように表示され、その後、デバウンスされた試行を更新するためにサーバーに 1 つのリクエストが送信される
新しいキャッシュエントリの作成または既存のキャッシュエントリの置換
既存のキャッシュエントリを作成または置換するには、upsertQueryData
を使用します。
upsertQueryData
は、既存のキャッシュエント리への「置換」、または新しいキャッシュエントリの「作成」を実行するように設計されています。upsertQueryData
はキャッシュエントリの前の状態にアクセスできないため、更新は置換としてのみ実行できます。比較すると、updateQueryData
は既存のキャッシュエントリの修正はできますが、新しいエントリを作成することはできません。
使用例の 1 つは楽観的な更新です。クライアントが Post
を作成するための API 呼び出しを行う場合、バックエンドは id
を含む完全なデータを返します。その後、upsertQueryData
を使用して getPostById(id)
クエリに対する新しいキャッシュエントリを作成し、後でアイテムを取得する追加のフェッチを防止できます。
レシピ
楽観的な更新
mutation
がトリガーされた直後にキャッシュデータに対する更新を実行したい場合は、楽観的な更新
を適用できます。これは、ミューテーションリクエストがまだ進行中であっても、変更がすぐに適用されたかのようにユーザーに感を与える場合に役立ちます。
楽観的な更新のコアコンセプトは次のとおりです
- クエリまたはミューテーションを開始すると、
onQueryStarted
が実行されます onQueryStarted
内でapi.util.updateQueryData
をディスパッチして手動でキャッシュデータを更新します- その後、
queryFulfilled
が拒否された場合- 前にディスパッチしたオブジェクトの
.undo
プロパティを使用してロールバックを実行するか、 api.util.invalidateTags
を使用してキャッシュデータを無効にして、データの完全な再フェッチをトリガーします
- 前にディスパッチしたオブジェクトの
多数のミューテーションが短期間に連続してトリガーされ、重なり合ったリクエストが発生する可能性がある場合は、障害が発生した場合に .undo
プロパティを使用してパッチをロールバックしようとすると競合状態が発生する可能性があります。このようなシナリオでは、代わりにエラー時にタグを無効にして、サーバーから本当に最新のデータを再フェッチする方が簡単で安全です。
- タイプスクリプト
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import type { Post } from './types'
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
getPost: build.query<Post, number>({
query: (id) => `post/${id}`,
providesTags: ['Post'],
}),
updatePost: build.mutation<void, Pick<Post, 'id'> & Partial<Post>>({
query: ({ id, ...patch }) => ({
url: `post/${id}`,
method: 'PATCH',
body: patch,
}),
async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
api.util.updateQueryData('getPost', id, (draft) => {
Object.assign(draft, patch)
})
)
try {
await queryFulfilled
} catch {
patchResult.undo()
/**
* Alternatively, on failure you can invalidate the corresponding cache tags
* to trigger a re-fetch:
* dispatch(api.util.invalidateTags(['Post']))
*/
}
},
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
getPost: build.query({
query: (id) => `post/${id}`,
providesTags: ['Post'],
}),
updatePost: build.mutation({
query: ({ id, ...patch }) => ({
url: `post/${id}`,
method: 'PATCH',
body: patch,
}),
async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
api.util.updateQueryData('getPost', id, (draft) => {
Object.assign(draft, patch)
})
)
try {
await queryFulfilled
} catch {
patchResult.undo()
/**
* Alternatively, on failure you can invalidate the corresponding cache tags
* to trigger a re-fetch:
* dispatch(api.util.invalidateTags(['Post']))
*/
}
},
}),
}),
})
または、.catch
が使用された少し短いバージョンを優先する場合
- async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
+ onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
api.util.updateQueryData('getPost', id, (draft) => {
Object.assign(draft, patch)
})
)
- try {
- await queryFulfilled
- } catch {
- patchResult.undo()
- }
+ queryFulfilled.catch(patchResult.undo)
}
例
悲観的な更新
mutation
がトリガーされた後にサーバーから受信した応答に基づいてキャッシュデータを更新する場合、悲観的な更新
を適用できます。悲観的な更新
と 楽観的な更新
の違いは、悲観的な更新
は、キャッシュされたデータを更新する前にサーバーからの応答を待機する点です。
悲観的な更新のコアコンセプトは次のとおりです。
- クエリまたはミューテーションを開始すると、
onQueryStarted
が実行されます queryFulfilled
が待機するオブジェクトは、data
プロパティに応答の変換結果が含まれます。onQueryStarted
内でapi.util.updateQueryData
をディスパッチすることで、サーバーからの応答内のデータを使用して、キャッシュされたデータを手動で更新します。onQueryStarted
内でapi.util.upsertQueryData
をディスパッチすることで、バックエンドによって返される完全な Post オブジェクトを使用して、手動で新しいキャッシュエントリを作成します。
- タイプスクリプト
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import type { Post } from './types'
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
getPost: build.query<Post, number>({
query: (id) => `post/${id}`,
providesTags: ['Post'],
}),
updatePost: build.mutation<Post, Pick<Post, 'id'> & Partial<Post>>({
query: ({ id, ...patch }) => ({
url: `post/${id}`,
method: 'PATCH',
body: patch,
}),
async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
try {
const { data: updatedPost } = await queryFulfilled
const patchResult = dispatch(
api.util.updateQueryData('getPost', id, (draft) => {
Object.assign(draft, updatedPost)
})
)
} catch {}
},
}),
createPost: build.mutation<Post, Pick<Post, 'id'> & Partial<Post>>({
query: ({ id, ...body }) => ({
url: `post/${id}`,
method: 'POST',
body,
}),
async onQueryStarted({ id }, { dispatch, queryFulfilled }) {
try {
const { data: createdPost } = await queryFulfilled
const patchResult = dispatch(
api.util.upsertQueryData('getPost', id, createdPost)
)
} catch {}
},
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
getPost: build.query({
query: (id) => `post/${id}`,
providesTags: ['Post'],
}),
updatePost: build.mutation({
query: ({ id, ...patch }) => ({
url: `post/${id}`,
method: 'PATCH',
body: patch,
}),
async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
try {
const { data: updatedPost } = await queryFulfilled
const patchResult = dispatch(
api.util.updateQueryData('getPost', id, (draft) => {
Object.assign(draft, updatedPost)
})
)
} catch {}
},
}),
createPost: build.mutation({
query: ({ id, ...body }) => ({
url: `post/${id}`,
method: 'POST',
body,
}),
async onQueryStarted({ id }, { dispatch, queryFulfilled }) {
try {
const { data: createdPost } = await queryFulfilled
const patchResult = dispatch(
api.util.upsertQueryData('getPost', id, createdPost)
)
} catch {}
},
}),
}),
})
一般的な更新
useDispatch フック(または、タイプ化バージョンの useAppDispatch(タイプスクリプトユーザー向け))を介して React コンポーネント内を含む、store.dispatch
メソッドにアクセスできる場合は、アプリケーションの他の場所にキャッシュデータを更新することができます。
正当な理由がない限り、ミューテーションの onQueryStarted
コールバックの外で手動でキャッシュを更新することは避ける必要があります。RTK Query は、キャッシュされたデータをサーバー側の状態の反映として扱うことを意図しています。
import { api } from './api'
import { useAppDispatch } from './store/hooks'
function App() {
const dispatch = useAppDispatch()
function handleClick() {
/**
* This will update the cache data for the query corresponding to the `getPosts` endpoint,
* when that endpoint is used with no argument (undefined).
*/
const patchCollection = dispatch(
api.util.updateQueryData('getPosts', undefined, (draftPosts) => {
draftPosts.push({ id: 1, name: 'Teddy' })
}),
)
}
return <button onClick={handleClick}>Add post to cache</button>
}