例:ショッピングサイトで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など)
- 引数として必要なデータを受け取り、
axios
やfetch
で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. 非同期処理の状態管理が自動でできる
普通のfetch
やaxios
だけ使うと、こんな状態管理を手動でやる必要があります
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. 失敗時・成功時の処理が明確に書ける
非同期処理が成功したとき・失敗したときに、onSuccess
やonError
で分離して安全に処理できる。
useMutation({
mutationFn: updateUser,
onSuccess: () => toast.success('更新成功'),
onError: () => toast.error('更新失敗'),
})
4. テストしやすい & 保守性が高い
- API処理(mutationFn)とUIロジックが明確に分離
- ステート管理やtry-catchがなくなり、コードがシンプルで読みやすくなる
気を付けるポイント
結論:「ブラックボックス化」「UIとのズレ」「設計の甘さ」につながる落とし穴もある。
1 . キャッシュを更新しないと画面が古いままになる
-
useMutation
はデフォルトでキャッシュを自動更新しない。 - たとえば「商品をカートに追加」しても、
invalidateQueries
やsetQueryData
しないと、画面が更新されない。
対策:
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、ロジックはサービス層などで切り分ける。