React QueryのuseMutationについて(Optimistic Update, invalidateQueries, AbortController)
はじめに
こんにちは、エンジニアのすずきです。
OPTEMOのフロントエンドではデータ取得などの非同期処理をRedux(redux-thunk)で行っているのですが、もっとスピード感をもって開発を行うために、現在、実装コストの低いReact QueryやSWRなどのデータ取得専用のライブラリへの置き換えを検討しています。
どのライブラリを使用するかはまだ決まっていないのですが、まずはReact Queryについていろいろ調査を行っています。
前回はReact Queryの基本事項を記事にしたので、今回はuseMutation()についてまとめました。
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);
},
},
);
さいごに
ジェイタマズではエンジニアを募集しています。
React/TypeScript/NestJSでモダンな開発をしてみたい!会社やサービスに興味がある!という方がいらっしゃいましたら、ぜひ気軽にカジュアル面談しましょう!
この記事が気に入ったらサポートをしてみませんか?