はじめに
React QueryのuseMutation()
でデータ更新の実装を行ったので記事にしました。
React Queryに関するこれまでの記事が気になる方は以下をご覧ください。
useMutation()
React Queryでは、サーバのデータ更新(create/update/delete)にuseMutation()
を使います。
mutationでデータが更新されるとき、invalidate queryでキャッシュが古くなった(stale)とみなし、キャッシュを更新します。
useMutation()
はuseQuery()
と似ているようで、以下のような違いがあります。
- キャッシュがない
- リトライしない
- 再フェッチしない
- mutate関数を返す
-
onMutate
というコールバックがある(Optimistic Updateのために使う)
useMutation()の型
TypeScriptを使う場合、mutate関数の型は以下のように定義されます。
useMutation<
TData, // mutationが返すデータの型
TError, // mutationが返すエラーの型
TVariables, // muatate関数の変数の型
TContext // onMutate内でセットされるコンテキストの型
>
invalidateQueries
mutationで更新したデータを再取得する場合、取得済みのキャッシュを明示的に破棄する必要があります。
QueryClientのインスタンスに対してinvalidateQueries
をコールすることで、キャッシュが古くなったもの(stale)とみなし、データ再フェッチのトリガーとすることができます。
例えば、useMutation()
のオプションonSettled
内でinvalidateQueries
を使うと、queryKeys.user
のキーをもつuseQuery()
のキャッシュを古くなったものとみなします。
onSettled: () => {
queryClient.invalidateQueries(queryKeys.user);
},
Optimistic Update
mutationの成功を期待して、サーバからレスポンスが返ってくる前にキャッシュを更新することをOptimistic Updateといいます。
画面表示を先に行ってからサーバのデータを更新することで、表示の待ち時間を削減するようなときに使うUXに関わる技術です。
これを実現するために、mutationが実行される前に発火するonMutate
関数を使います。
以下の処理を記述することでOptimistic Updateを行います。
onMutate: async (newData: User | null) => {
// Optimistic Updateが古いデータで上書きされないようにクエリをキャンセルする
queryClient.cancelQueries(queryKeys.user);
// キャッシュから更新前データのスナップショットをとる
const previousUserData: User = queryClient.getQueryData(queryKeys.user);
// 新しいデータでキャッシュを更新する(Optimistic Update)
updateUser(newData);
// スナップショットからContextのオブジェクトを返す(onErrorやonSettledに渡せる)
return { previousUserData };
},
キャッシュのロールバック
もしmutationが失敗した場合、onMutate
の戻り値であるContext(previousUserData)をonError
に渡し、ロールバックの処理を実行させることができます。
onError: (error, newData, context) => {
if (context.previousUserData) {
updateUser(context.previousUserData);
}
},
mutationが失敗した場合の処理の実行順はonMutate→mutation→onError→onSettled
となり、成功した場合はonMutate→mutation→onSuccess→onSettled
となります。
クエリのキャンセル
Optimistic Updateの項で、クエリのキャンセルのためにqueryClient.cancelQueries(queryKeys.user)
を記述しましたが、この部分の処理には、フェッチのような非同期処理を途中で止めるためのWeb APIであるAbortController
を使用しています。
fetch APIのRequestでAbortSignal
をオプションとして受け取ることができ、fetchの第二引数にAbortSignal
を渡すことで、fetchを中断させる事ができます。
async function getUser(
user: User | null,
signal: AbortSignal,
): Promise<User | null> {
if (!user) return null;
const { data }: AxiosResponse<{ user: User }> = await axiosInstance.get(
`/user/${user.id}`,
{
headers: getJWTHeader(user),
signal,
},
);
return data.user;
}
queryClient.cancelQueries(queryKeys.user)
を実行すると、queryKeys.user
をキーをもつuseQuery()
経由でgetUser()
にAbortSignal
が渡り、フェッチが中断されます。
const { data: user } = useQuery(
queryKeys.user,
({ signal }) => getUser(user, signal),
...
)
最後に
今回の説明に用いたuseMutation()
の中身を以下に記述します。
const { mutate } = useMutation(
(newUserData: User) => patchUserOnServer(newUserData, user),
{
onMutate: async (newData: User | null) => {
queryClient.cancelQueries(queryKeys.user);
const previousUserData: User = queryClient.getQueryData(queryKeys.user);
updateUser(newData);
return { previousUserData };
},
onError: (error, newData, context) => {
if (context.previousUserData) {
updateUser(context.previousUserData);
}
},
onSuccess: (userData: User | null) => {
if (user) {
updateUser(userData);
}
},
onSettled: () => {
queryClient.invalidateQueries(queryKeys.user);
},
},
);
参考資料