メインコンテンツにスキップ

手動キャッシュ更新

概要

ほとんどの場合、バックエンドの変更をトリガーした後に最新データを受信するには、キャッシュタグの無効化を利用して自動再フェッチを実行できます。この機能を使用すると、データが無効になるようなミューテーションが発生したことが通知されると、クエリによってデータが再フェッチされます。

ほとんどの場合、手動キャッシュ更新よりも自動再フェッチを使用することをお勧めします。

しかし、「楽観的」または「悲観的」更新、またはキャッシュエントリライフサイクルの一環としてのデータ変更など、手動キャッシュ更新が必要なユースケースもあります

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 プロパティを使用してパッチをロールバックしようとすると競合状態が発生する可能性があります。このようなシナリオでは、代わりにエラー時にタグを無効にして、サーバーから本当に最新のデータを再フェッチする方が簡単で安全です。

楽観的な更新ミューテーションの例(async await)
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']))
*/
}
},
}),
}),
})

または、.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)
}

React の楽観的な更新

悲観的な更新

mutation がトリガーされた後にサーバーから受信した応答に基づいてキャッシュデータを更新する場合、悲観的な更新 を適用できます。悲観的な更新楽観的な更新 の違いは、悲観的な更新 は、キャッシュされたデータを更新する前にサーバーからの応答を待機する点です。

悲観的な更新のコアコンセプトは次のとおりです。

  • クエリまたはミューテーションを開始すると、onQueryStarted が実行されます
  • queryFulfilled が待機するオブジェクトは、data プロパティに応答の変換結果が含まれます。
  • onQueryStarted 内で api.util.updateQueryData をディスパッチすることで、サーバーからの応答内のデータを使用して、キャッシュされたデータを手動で更新します。
  • onQueryStarted 内で api.util.upsertQueryData をディスパッチすることで、バックエンドによって返される完全な Post オブジェクトを使用して、手動で新しいキャッシュエントリを作成します。
悲観的な更新ミューテーションの例(async await)
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 {}
},
}),
}),
})

一般的な更新

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>
}