0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ショッピングサイトを例にしたuseMutationの使い方

Posted at

例:ショッピングサイトでuseMutationが使われる場面

1. カートに商品を追加(POST)

import { useMutation } from '@tanstack/react-query'
import axios from 'axios'

// API呼び出し関数
const addToCart = async ({ productId, quantity }) => {
  return await axios.post('/api/cart', { productId, quantity })
}

// useMutationの定義
const { mutate, isPending, isSuccess, isError } = useMutation({
  mutationFn: addToCart,
  onSuccess: () => {
    console.log('カートに追加しました')
  },
  onError: (error) => {
    console.error('追加に失敗しました', error)
  }
})

// ボタンで呼び出す
<button onClick={() => mutate({ productId: 1, quantity: 2 })}>
  カートに追加
</button>

2. カートから商品を削除(DELETE)

const deleteCartItem = async (productId) => {
  return await axios.delete(`/api/cart/${productId}`)
}

const { mutate: deleteItem } = useMutation({
  mutationFn: deleteCartItem,
  onSuccess: () => {
    console.log('商品を削除しました')
  }
})

// 削除ボタン
<button onClick={() => deleteItem(1)}>
  削除
</button>

3. カート内の数量を変更(PATCH)

const updateCartQuantity = async ({ productId, quantity }) => {
  return await axios.patch(`/api/cart/${productId}`, { quantity })
}

const { mutate: updateQuantity } = useMutation({
  mutationFn: updateCartQuantity,
  onSuccess: () => {
    console.log('数量を更新しました')
  }
})

// 数量変更時に呼ぶ
<input
  type="number"
  defaultValue={1}
  onBlur={(e) =>
    updateQuantity({ productId: 1, quantity: Number(e.target.value) })
  }
/>

共通の全体フロー

1. 非同期処理(mutationFn)を定義する

  • 実際にサーバーへリクエストを送る関数(POST/DELETE/PATCHなど)
  • 引数として必要なデータを受け取り、axiosfetchでAPI呼び出し
const doSomething = async (data) => {
  return await axios.post('/api/something', data)
}

2. useMutationでmutationFnを登録する

  • React QueryのuseMutationに、↑で作った関数を登録
  • 状態やコールバック(onSuccess/onErrorなど)を指定
const mutation = useMutation({
  mutationFn: doSomething,
  onSuccess: () => { /* 成功したときの処理 */ },
  onError: (error) => { /* エラー時の処理 */ },
})

状態もここで使えるようになる

  • mutation.mutate:処理を実行する関数
  • mutation.isPending:実行中
  • mutation.isSuccess:成功後
  • mutation.isError:失敗したとき
  • mutation.error:エラーの内容
  • mutation.data:成功時の返却データ

3. ユーザー操作などでmutate()を実行する

  • 実行タイミングはボタン押下や入力イベントなど
  • mutate(data)として、必要な引数を渡す
<button onClick={() => mutation.mutate({ productId: 1, quantity: 2 })}>
  実行する
</button>

4. 成功・失敗・ローディングに応じてUIを制御

  • 状態に応じて、メッセージやローディング、ボタン制御を行う
{mutation.isPending && <p>処理中...</p>}
{mutation.isSuccess && <p>成功しました!</p>}
{mutation.isError && <p>エラー:{mutation.error.message}</p>}

5. 必要に応じてキャッシュ更新・画面更新を行う

  • 成功後に、一覧画面などのデータを再取得したい場合
  • queryClient.invalidateQueries()などでキャッシュを無効化して再フェッチ
const queryClient = useQueryClient()

useMutation({
  mutationFn: doSomething,
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['itemList'] })
  }
})


useMutationの便利なポイント

結論:「非同期処理 + UI更新 + エラーハンドリング」を最小のコードで、統一的に、安全に扱えるようになる。

1. 非同期処理の状態管理が自動でできる

普通のfetchaxiosだけ使うと、こんな状態管理を手動でやる必要があります

const [isLoading, setIsLoading] = useState(false)
const [isError, setIsError] = useState(false)

const handleSubmit = async () => {
  try {
    setIsLoading(true)
    await axios.post(...)
    setIsLoading(false)
  } catch (e) {
    setIsError(true)
    setIsLoading(false)
  }
}

useMutationを使えば、これが不要になります。

const mutation = useMutation({ mutationFn: myPost })

// UIで状態をそのまま使える
{mutation.isPending && <p>送信中...</p>}
{mutation.isError && <p>エラー!</p>}

2. 成功時にキャッシュ更新が簡単

const queryClient = useQueryClient()

useMutation({
  mutationFn: postItem,
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['cart'] })
  }
})

→ 商品を追加したあと、自動的にカート一覧を最新状態に更新できる。

3. 失敗時・成功時の処理が明確に書ける

非同期処理が成功したとき・失敗したときに、onSuccessonError分離して安全に処理できる

useMutation({
  mutationFn: updateUser,
  onSuccess: () => toast.success('更新成功'),
  onError: () => toast.error('更新失敗'),
})

4. テストしやすい & 保守性が高い

  • API処理(mutationFn)とUIロジックが明確に分離
  • ステート管理やtry-catchがなくなり、コードがシンプルで読みやすくなる

気を付けるポイント

結論:「ブラックボックス化」「UIとのズレ」「設計の甘さ」につながる落とし穴もある。

1 . キャッシュを更新しないと画面が古いままになる

  • useMutationデフォルトでキャッシュを自動更新しない
  • たとえば「商品をカートに追加」しても、invalidateQueriessetQueryDataしないと、画面が更新されない。

対策

onSuccess: () => {
  queryClient.invalidateQueries({ queryKey: ['cart'] })
}

2 . UIの状態と非同期の結果がズレることがある

  • mutation.isSuccessなどを使ってUI制御していても、別画面に遷移するときやリセットがないと成功状態がずっと残る
  • 例:送信完了メッセージが消えない。

対策

useEffect(() => {
  if (mutation.isSuccess) {
    mutation.reset()
  }
}, [mutation.isSuccess])

3 . 複雑な処理を1つのmutateに押し込んでしまう

  • mutate()の中で副作用やUI制御、状態更新などを全部やってしまうと、責務が曖昧になりバグの温床に

対策:API呼び出しとUI制御の責務を分離する。

4 . エラーの扱いが雑になる

  • onErrorでログ出すだけで終わっていると、ユーザーに何も伝わらない
  • バリデーションエラー・認証エラー・ネットワークエラーの区別ができないことも。

対策

onError: (error) => {
  if (axios.isAxiosError(error) && error.response?.status === 400) {
    toast.error('入力内容に誤りがあります')
  } else {
    toast.error('サーバーエラーが発生しました')
  }
}

5 . 複数のmutationが絡むとロジックが破綻しやすい

  • 例:商品を追加→在庫を更新→通知を送る…と依存関係のあるmutationを順に使うのが難しい
  • ミスると順番通りに動かない or 状態がおかしくなる。

対策async/awaitで連続実行したり、必要ならuseMutationをネストせず共通の処理関数にまとめる。

6.なんでもmutationに頼る設計になる

  • useMutationで何でも済ませようとすると、ロジックとUIが混在した設計になりがち
  • 状態管理ライブラリ(例:Zustand, Recoil)との住み分けが曖昧に。

対策:UIはUI、ロジックはサービス層などで切り分ける。

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?