見出し画像

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でモダンな開発をしてみたい!会社やサービスに興味がある!という方がいらっしゃいましたら、ぜひ気軽にカジュアル面談しましょう!


この記事が気に入ったらサポートをしてみませんか?