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

# はじめに
React Queryの`useMutation()`でデータ更新の実装を行ったので記事にしました。

React Queryに関するこれまでの記事が気になる方は以下をご覧ください。

https://qiita.com/suzuki0430/items/fbcd0ce8ee10f91e3d92

https://qiita.com/suzuki0430/items/6afa25b28d96612aa429

https://qiita.com/suzuki0430/items/9b2db0c23a4541964415

# useMutation()
React Queryでは、サーバのデータ更新(create/update/delete)に`useMutation()`を使います。
mutationでデータが更新されるとき、invalidate queryでキャッシュが古くなった(stale)とみなし、キャッシュを更新します。

`useMutation()`は`useQuery()`と似ているようで、以下のような違いがあります。

- キャッシュがない
- リトライしない
- 再フェッチしない
- mutate関数を返す
- `onMutate`というコールバックがある(Optimistic Updateのために使う)

## useMutation()の型
TypeScriptを使う場合、mutate関数の型は以下のように定義されます。

```ts
useMutation<
  TData, // mutationが返すデータの型
  TError, // mutationが返すエラーの型
  TVariables, // muatate関数の変数の型
  TContext // onMutate内でセットされるコンテキストの型
>
```

## invalidateQueries
mutationで更新したデータを再取得する場合、取得済みのキャッシュを明示的に破棄する必要があります。
QueryClientのインスタンスに対して`invalidateQueries`をコールすることで、キャッシュが古くなったもの(stale)とみなし、データ再フェッチのトリガーとすることができます。

例えば、`useMutation()`のオプション`onSettled`内で`invalidateQueries`を使うと、`queryKeys.user`のキーをもつ`useQuery()`のキャッシュを古くなったものとみなします。

```ts
      onSettled: () => {
        queryClient.invalidateQueries(queryKeys.user);
      },
```

## Optimistic Update
mutationの成功を期待して、サーバからレスポンスが返ってくる前にキャッシュを更新することをOptimistic Updateといいます。
画面表示を先に行ってからサーバのデータを更新することで、表示の待ち時間を削減するようなときに使うUXに関わる技術です。

これを実現するために、mutationが実行される前に発火する`onMutate`関数を使います。
以下の処理を記述することでOptimistic Updateを行います。

```ts
      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`に渡し、ロールバックの処理を実行させることができます。

```ts
      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を中断させる事ができます。

```ts
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`が渡り、フェッチが中断されます。

```ts
  const { data: user } = useQuery(
    queryKeys.user,
    ({ signal }) => getUser(user, signal),
    ...
  )
```

# 最後に
今回の説明に用いた`useMutation()`の中身を以下に記述します。

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


# 参考資料

https://www.udemy.com/share/104FBU3@ALGCLpGr3Vcm7IYP7g9jdeT8KWVxNPlyxMYs2jsIMI56oYrxwBgKo2jJMTlAuYtr/

https://react-query.tanstack.com/reference/useMutation

https://tech.studyplus.co.jp/entry/2021/05/18/100000

https://kaminashi-developer.hatenablog.jp/entry/optimistic-update-in-spa

https://oita.oika.me/2021/09/06/react-query-reset-vs-invalidate

https://www.mitsue.co.jp/knowledge/blog/frontend/202012/14_0900.html#:~:text=AbortController%20%E3%81%AF%E7%B0%A1%E5%8D%98%E3%81%AB%E8%A8%80%E3%81%86,%E5%AE%9F%E8%A3%85%E3%81%95%E3%82%8C%E3%81%9F%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%95%E3%82%A7%E3%83%BC%E3%82%B9%E3%81%A7%E3%81%99%E3%80%82