ミューテーション
概要
ミューテーションは、サーバーにデータ更新を送信し、ローカルキャッシュに変更を適用するために使用されます。ミューテーションは、キャッシュされたデータを無効化し、強制的に再フェッチすることもできます。
ミューテーションエンドポイントの定義
ミューテーションエンドポイントは、createApi
のendpoints
セクション内でオブジェクトを返すことで定義され、build.mutation()
メソッドを使用してフィールドを定義します。
ミューテーションエンドポイントは、URL(URLクエリパラメータを含む)を構築するquery
コールバック、または任意の非同期ロジックを実行して結果を返すことができるqueryFn
コールバックのいずれかを定義する必要があります。query
コールバックは、URL、使用するHTTPメソッド、リクエストボディを含むオブジェクトを返すこともできます。
query
コールバックがURLを生成するために追加のデータを必要とする場合は、単一の引数を取るように記述する必要があります。複数のパラメータを渡す必要がある場合は、単一の「オプションオブジェクト」としてフォーマットして渡します。
ミューテーションエンドポイントは、結果がキャッシュされる前に応答内容を変更したり、キャッシュの無効化を識別するための「タグ」を定義したり、キャッシュエントリが追加および削除される際に追加のロジックを実行するためのキャッシュエントリライフサイクルコールバックを提供することもできます。
TypeScriptで使用する場合は、戻り値の型と予期されるクエリ引数のジェネリクスを指定する必要があります。build.mutation<ReturnType, ArgType>
。引数がない場合は、引数型にvoid
を使用します。
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import type { Post } from './types'
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
// The mutation accepts a `Partial<Post>` arg, and returns a `Post`
updatePost: build.mutation<Post, Partial<Post> & Pick<Post, 'id'>>({
// note: an optional `queryFn` may be used in place of `query`
query: ({ id, ...patch }) => ({
url: `post/${id}`,
method: 'PATCH',
body: patch,
}),
// Pick out data and prevent nested properties in a hook or selector
transformResponse: (response: { data: Post }, meta, arg) => response.data,
// Pick out errors and prevent nested properties in a hook or selector
transformErrorResponse: (
response: { status: string | number },
meta,
arg
) => response.status,
invalidatesTags: ['Post'],
// onQueryStarted is useful for optimistic updates
// The 2nd parameter is the destructured `MutationLifecycleApi`
async onQueryStarted(
arg,
{ dispatch, getState, queryFulfilled, requestId, extra, getCacheEntry }
) {},
// The 2nd parameter is the destructured `MutationCacheLifecycleApi`
async onCacheEntryAdded(
arg,
{
dispatch,
getState,
extra,
requestId,
cacheEntryRemoved,
cacheDataLoaded,
getCacheEntry,
}
) {},
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
// The mutation accepts a `Partial<Post>` arg, and returns a `Post`
updatePost: build.mutation({
// note: an optional `queryFn` may be used in place of `query`
query: ({ id, ...patch }) => ({
url: `post/${id}`,
method: 'PATCH',
body: patch,
}),
// Pick out data and prevent nested properties in a hook or selector
transformResponse: (response, meta, arg) => response.data,
// Pick out errors and prevent nested properties in a hook or selector
transformErrorResponse: (response, meta, arg) => response.status,
invalidatesTags: ['Post'],
// onQueryStarted is useful for optimistic updates
// The 2nd parameter is the destructured `MutationLifecycleApi`
async onQueryStarted(
arg,
{ dispatch, getState, queryFulfilled, requestId, extra, getCacheEntry }
) {},
// The 2nd parameter is the destructured `MutationCacheLifecycleApi`
async onCacheEntryAdded(
arg,
{
dispatch,
getState,
extra,
requestId,
cacheEntryRemoved,
cacheDataLoaded,
getCacheEntry,
}
) {},
}),
}),
})
onQueryStarted
メソッドは、オプティミスティックアップデートに使用できます。
React Hooksを使用したミューテーションの実行
ミューテーションフックの振る舞い
useQuery
とは異なり、useMutation
はタプルを返します。タプルの最初の項目は「トリガー」関数で、2番目の要素にはstatus
、error
、data
を含むオブジェクトが含まれます。
useQuery
フックとは異なり、useMutation
フックは自動的に実行されません。ミューテーションを実行するには、フックから最初のタプル値として返されたトリガー関数を呼び出す必要があります。
フックのシグネチャと詳細については、useMutation
を参照してください。
よく使用されるミューテーションフックの戻り値
useMutation
フックは、「ミューテーショントリガー」関数と、「ミューテーション結果」に関するプロパティを含むオブジェクトを含むタプルを返します。
「ミューテーショントリガー」は、呼び出されると、そのエンドポイントに対するミューテーションリクエストを発行する関数です。「ミューテーショントリガー」を呼び出すと、unwrap
プロパティを持つPromiseが返されます。これは、ミューテーション呼び出しをアンラップし、未処理の応答/エラーを提供するために呼び出すことができます。これは、ミューテーションが呼び出しサイトでインラインで成功/失敗するかどうかを判断したい場合に役立ちます。
「ミューテーション結果」は、ミューテーションリクエストの最新のdata
や、現在のリクエストライフサイクル状態のステータスブール値などのプロパティを含むオブジェクトです。
以下は、「ミューテーション結果」オブジェクトで最も頻繁に使用されるプロパティの一部です。返されるすべてのプロパティの詳細なリストについては、useMutation
を参照してください。
data
- 最新のトリガー応答から返されたデータ(存在する場合)。同じフックインスタンスからの後続のトリガーが呼び出された場合、新しいデータが受信されるまで、これは未定義を返します。新しいデータへのスムーズな移行のために前の応答データが必要な場合は、コンポーネントレベルのキャッシュを検討してください。error
- エラー結果(存在する場合)。isUninitialized
- trueの場合、ミューテーションがまだ発行されていないことを示します。isLoading
- trueの場合、ミューテーションが発行され、応答を待っていることを示します。isSuccess
- trueの場合、最後に発行されたミューテーションに成功したリクエストからのデータがあることを示します。isError
- trueの場合、最後に発行されたミューテーションがエラー状態になったことを示します。reset
- フックを元の状態に戻し、キャッシュから現在の結果を削除するメソッド
RTK Queryでは、ミューテーションはクエリのように「ロード中」と「フェッチ中」の間の意味的な区別を含んでいません。ミューテーションの場合、後続の呼び出しは必ずしも関連性があるとは想定されないため、ミューテーションは「ロード中」か「非ロード中」のいずれかであり、「再フェッチ」の概念はありません。
ミューテーション結果の共有
デフォルトでは、useMutation
フックの個別のインスタンスは、本質的に互いに関連していません。あるインスタンスをトリガーしても、別のインスタンスの結果には影響しません。これは、フックが同じコンポーネント内または異なるコンポーネント内で呼び出されるかどうかに関係なく適用されます。
export const ComponentOne = () => {
// Triggering `updatePostOne` will affect the result in this component,
// but not the result in `ComponentTwo`, and vice-versa
const [updatePost, result] = useUpdatePostMutation()
return <div>...</div>
}
export const ComponentTwo = () => {
const [updatePost, result] = useUpdatePostMutation()
return <div>...</div>
}
RTK Queryは、fixedCacheKey
オプションを使用して、ミューテーションフックインスタンス間で結果を共有するオプションを提供します。同じfixedCacheKey
文字列を持つuseMutation
フックは、トリガー関数のいずれかが呼び出されたときに、相互に結果を共有します。これは、結果を共有したい各ミューテーションフックインスタンス間で共有される一意の文字列である必要があります。
export const ComponentOne = () => {
// Triggering `updatePostOne` will affect the result in both this component,
// but as well as the result in `ComponentTwo`, and vice-versa
const [updatePost, result] = useUpdatePostMutation({
fixedCacheKey: 'shared-update-post',
})
return <div>...</div>
}
export const ComponentTwo = () => {
const [updatePost, result] = useUpdatePostMutation({
fixedCacheKey: 'shared-update-post',
})
return <div>...</div>
}
fixedCacheKey
を使用する場合、originalArgs
プロパティは共有できず、常にundefined
になります。
標準的なミューテーションの例
これは、ページの下部にある完全な例の修正版で、updatePost
ミューテーションを強調表示しています。このシナリオでは、投稿がuseQuery
でフェッチされ、投稿の名前を編集できるEditablePostName
コンポーネントがレンダリングされます。
export const PostDetail = () => {
const { id } = useParams<{ id: any }>()
const { data: post } = useGetPostQuery(id)
const [
updatePost, // This is the mutation trigger
{ isLoading: isUpdating }, // This is the destructured mutation result
] = useUpdatePostMutation()
return (
<Box p={4}>
<EditablePostName
name={post.name}
onUpdate={(name) => {
// If you want to immediately access the result of a mutation, you need to chain `.unwrap()`
// if you actually want the payload or to catch the error.
// Example: `updatePost().unwrap().then(fulfilled => console.log(fulfilled)).catch(rejected => console.error(rejected))
return (
// Execute the trigger with the `id` and updated `name`
updatePost({ id, name })
)
}}
isLoading={isUpdating}
/>
</Box>
)
}
再検証を伴う高度なミューテーション
現実世界では、開発者がミューテーションを実行した後、ローカルデータキャッシュをサーバーと再同期したい(別名「再検証」)のは非常に一般的です。RTK Queryは、これに対してより集中型のアプローチを取り、APIサービス定義で無効化動作を構成する必要があります。RTK Queryを使用した高度な無効化処理の詳細については、抽象タグIDを使用した高度な無効化を参照してください。
再検証の例
これは、投稿のCRUDサービスの例です。これは、リストを選択的に無効にする戦略を実装しており、実際のアプリケーションの優れた基盤となる可能性が最も高いです。
- TypeScript
- JavaScript
// Or from '@reduxjs/toolkit/query' if not using the auto-generated hooks
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export interface Post {
id: number
name: string
}
type PostsResponse = Post[]
export const postApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Posts'],
endpoints: (build) => ({
getPosts: build.query<PostsResponse, void>({
query: () => 'posts',
// Provides a list of `Posts` by `id`.
// If any mutation is executed that `invalidate`s any of these tags, this query will re-run to be always up-to-date.
// The `LIST` id is a "virtual id" we just made up to be able to invalidate this query specifically if a new `Posts` element was added.
providesTags: (result) =>
// is result available?
result
? // successful query
[
...result.map(({ id }) => ({ type: 'Posts', id } as const)),
{ type: 'Posts', id: 'LIST' },
]
: // an error occurred, but we still want to refetch this query when `{ type: 'Posts', id: 'LIST' }` is invalidated
[{ type: 'Posts', id: 'LIST' }],
}),
addPost: build.mutation<Post, Partial<Post>>({
query(body) {
return {
url: `post`,
method: 'POST',
body,
}
},
// Invalidates all Post-type queries providing the `LIST` id - after all, depending of the sort order,
// that newly created post could show up in any lists.
invalidatesTags: [{ type: 'Posts', id: 'LIST' }],
}),
getPost: build.query<Post, number>({
query: (id) => `post/${id}`,
providesTags: (result, error, id) => [{ type: 'Posts', id }],
}),
updatePost: build.mutation<Post, Partial<Post>>({
query(data) {
const { id, ...body } = data
return {
url: `post/${id}`,
method: 'PUT',
body,
}
},
// Invalidates all queries that subscribe to this Post `id` only.
// In this case, `getPost` will be re-run. `getPosts` *might* rerun, if this id was under its results.
invalidatesTags: (result, error, { id }) => [{ type: 'Posts', id }],
}),
deletePost: build.mutation<{ success: boolean; id: number }, number>({
query(id) {
return {
url: `post/${id}`,
method: 'DELETE',
}
},
// Invalidates all queries that subscribe to this Post `id` only.
invalidatesTags: (result, error, id) => [{ type: 'Posts', id }],
}),
}),
})
export const {
useGetPostsQuery,
useAddPostMutation,
useGetPostQuery,
useUpdatePostMutation,
useDeletePostMutation,
} = postApi
// Or from '@reduxjs/toolkit/query' if not using the auto-generated hooks
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const postApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Posts'],
endpoints: (build) => ({
getPosts: build.query({
query: () => 'posts',
// Provides a list of `Posts` by `id`.
// If any mutation is executed that `invalidate`s any of these tags, this query will re-run to be always up-to-date.
// The `LIST` id is a "virtual id" we just made up to be able to invalidate this query specifically if a new `Posts` element was added.
providesTags: (result) =>
// is result available?
result
? // successful query
[
...result.map(({ id }) => ({ type: 'Posts', id })),
{ type: 'Posts', id: 'LIST' },
]
: // an error occurred, but we still want to refetch this query when `{ type: 'Posts', id: 'LIST' }` is invalidated
[{ type: 'Posts', id: 'LIST' }],
}),
addPost: build.mutation({
query(body) {
return {
url: `post`,
method: 'POST',
body,
}
},
// Invalidates all Post-type queries providing the `LIST` id - after all, depending of the sort order,
// that newly created post could show up in any lists.
invalidatesTags: [{ type: 'Posts', id: 'LIST' }],
}),
getPost: build.query({
query: (id) => `post/${id}`,
providesTags: (result, error, id) => [{ type: 'Posts', id }],
}),
updatePost: build.mutation({
query(data) {
const { id, ...body } = data
return {
url: `post/${id}`,
method: 'PUT',
body,
}
},
// Invalidates all queries that subscribe to this Post `id` only.
// In this case, `getPost` will be re-run. `getPosts` *might* rerun, if this id was under its results.
invalidatesTags: (result, error, { id }) => [{ type: 'Posts', id }],
}),
deletePost: build.mutation({
query(id) {
return {
url: `post/${id}`,
method: 'DELETE',
}
},
// Invalidates all queries that subscribe to this Post `id` only.
invalidatesTags: (result, error, id) => [{ type: 'Posts', id }],
}),
}),
})
export const {
useGetPostsQuery,
useAddPostMutation,
useGetPostQuery,
useUpdatePostMutation,
useDeletePostMutation,
} = postApi