LoginSignup
41
17

More than 1 year has passed since last update.

【React】React QueryのuseMutation()でデータ更新を行う(Optimistic Update, invalidateQueries, AbortController)

Last updated at Posted at 2021-12-15

はじめに

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);
      },
    },
  );

参考資料

41
17
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
17