Vue Query 使用 useMutation 來處理 API 變更
不喜歡看字的可以看影片:
- Vue3 教學 - Vue Query Part.1 使用 useQuery 與 useMutation
- Vue3 教學 - Vue Query Part.2 資料生命週期與重新獲取機制 (stale,fresh,paused,inActive)
- Vue3 教學 - Vue Query Part.3 使用 placeholderData 與 keepPreviousData 提升分頁體驗
- Vue3 教學 - Vue Query Part.4 使用 useMutation 進行樂觀更新
- Vue3 教學 - Vue Query Part.5 如何使用 enabled 控制查詢與 prefetch 提升使用者體驗
簡介
在 Vue 應用中,當我們需要 新增、更新或刪除 API 資料時,傳統的做法通常是:
- 發送
POST、PUT或DELETE請求。 - 管理請求的
loading狀態,確保 UI 在請求進行中不會產生錯誤行為。 - 處理請求的錯誤與成功狀態,例如顯示錯誤訊息或通知用戶操作成功。
- 手動更新前端狀態,確保資料與後端同步。
- 手動重新獲取 API 資料,確保其他元件顯示最新的數據。
這種做法雖然可行,但當應用變大時,管理這些請求變得非常麻煩,這時 Vue Query 的 useMutation 就能幫上忙!
| 傳統方式 | Vue Query useMutation |
|---|---|
| 需要手動管理 loading 狀態 | 內建 isPending 狀態 |
| 需要手動處理錯誤 | 內建 onError 處理錯誤 |
| 需要手動更新快取 | onSuccess 自動更新快取 |
| 需要手動控制重試邏輯 | 內建 retry 機制 |
為什麼要使用 useMutation?
useMutation 主要解決 非同步 API 變更的三大問題:
- 自動管理請求狀態:提供
isPending、isError、isSuccess等狀態,省去手動追蹤請求進行中的邏輯。 - 內建錯誤處理與重試機制:可以設定
retry來自動重試請求,提升穩定性。 - 自動更新快取:可以在 API 成功後自動更新快取,減少不必要的 API 重新請求。
useMutation 的基本用法
新增 Todo 資料
因為 jsonplaceholder 的 API 不允許我們新增資料,所以想要看到效果的可以自己架設 json-server 來測試,影片也有提到。
顯示資料一樣用上一篇的 Todos.vue,這邊就不重複了,只是把 API 改成 json-server 的 API。
<script setup>
import { useQuery } from "@tanstack/vue-query";
import axios from "axios";
const fetchTodos = async () => {
const { data } = await axios.get("http://localhost:3002/todos");
return data;
};
const {
data: todos,
isLoading,
isError,
} = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
gcTime: 3000,
});
</script>
<template>
<div>
{{ isLoading }}
<p v-if="isLoading">載入中...</p>
<p v-else-if="isError">取得資料失敗</p>
<ul v-else>
<li v-for="todo in todos" :key="todo.id">{{ todo.title }}</li>
</ul>
</div>
</template>
新增資料的元件我們稱為 CreateTodo.vue,這邊我們會使用 useMutation 來處理新增資料的請求。
我們在 useMutation 中,需要傳入一個 mutationFn 函式,這個函式會接收一個參數,這個參數就是我們要新增的資料。
之後將 mutate 從 useMutation 中拿出來,並且綁定到 button 的 @click 事件中,當按下按鈕時,就會發送請求。
在請求發出去的時候,會進入到 isPending 狀態,請求成功後,會進入到 isSuccess 狀態,請求失敗後,會進入到 isError 狀態。
<script setup>
import { useMutation } from "@tanstack/vue-query";
import axios from "axios";
// 定義一個 API 請求函式
const createTodo = async (newTodo) => {
const { data } = await axios.post("http://localhost:3002/todos", newTodo);
return data;
};
// 使用 useMutation 來管理這個請求
const { mutate, isPending, isError, isSuccess } = useMutation({
mutationFn: createTodo
});
const handleCreateTodo = () => {
mutate({ id: crypto.randomUUID(), title: "新待辦事項" });
};
</script>
<template>
<div>
<button @click="handleCreateTodo" :disabled="isPending">
{{ isPending ? "新增中..." : "新增 Todo" }}
</button>
<p v-if="isSuccess">成功新增!</p>
<p v-if="isError">發生錯誤,請稍後再試</p>
</div>
</template>
<script setup>
import { VueQueryDevtools } from "@tanstack/vue-query-devtools";
import Todos from "./components/Todos.vue";
import AddTodo from "./components/CreateTodo.vue";
</script>
<template>
<AddTodo />
<Todos />
<VueQueryDevtools />
</template>
跨元件重新請求
在 CreateTodo.vue 中,觸發了 mutate 後,會將資料新增到 json-server 中,這時候我們在 Todos.vue 中,會發現資料並沒有即時更新。
但我們渲染資料的地方是在 Todos.vue 中,而新增資料的地方是在 CreateTodo.vue,所以如果要跨元件重新請求資料,可以引入 useQueryClient 並在 CreateTodo.vue 中,mutate 成功後,使用 queryClient.invalidateQueries 來重新請求資料。
在 invalidateQueries 中,我們需要傳入 queryKey,這個 queryKey 就是我們在 useQuery 中設定的 queryKey,這樣就可以讓 Todos.vue 中的資料即時更新。
<script setup>
import { useMutation, useQueryClient } from "@tanstack/vue-query";
import axios from "axios";
const queryClient = useQueryClient();
// 定義一個 API 請求函式
const createTodo = async (newTodo) => {
const { data } = await axios.post("http://localhost:3002/todos", newTodo);
return data;
};
// 使用 useMutation 來管理這個請求
const { mutate, isPending, isError, isSuccess } = useMutation({
mutationFn: createTodo,
onSuccess: () => {
// 當請求成功時,強制更新快取,讓資料保持同步
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
const handleCreateTodo = () => {
mutate({ id: crypto.randomUUID(), title: "新待辦事項" });
};
</script>
<template>
<div>
<button @click="handleCreateTodo" :disabled="isPending">
{{ isPending ? "新增中..." : "新增 Todo" }}
</button>
<p v-if="isSuccess">成功新增!</p>
<p v-if="isError">發生錯誤,請稍後再試</p>
</div>
</template>
同元件重新請求
順便補充一下,如果我們在同一個元件中,想要重新請求資料,可以透過 useQuery 的 refetch 來重新請求資料。
<script setup>
import { useQuery } from "@tanstack/vue-query";
import axios from "axios";
const fetchTodos = async () => {
const { data } = await axios.get("http://localhost:3002/todos");
return data;
};
const {
data: todos,
isLoading,
isError,
refetch,
} = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos
});
const handleRefetch = () => {
refetch();
};
</script>
<template>
<div>
<button @click="handleRefetch">重新請求</button>
<p v-if="isLoading">載入中...</p>
<p v-else-if="isError">取得資料失敗</p>
<ul v-else>
<li v-for="todo in todos" :key="todo.id">{{ todo.title }}</li>
</ul>
</div>
</template>
mutate 和 mutateAsync 的差別
mutate 和 mutateAsync 的差別在於,mutate 是同步的,而 mutateAsync 是非同步的。
mutateAsync 會回傳一個 Promise,所以可以透過 await 來等待請求完成。
| 特性 | mutate | mutateAsync |
|---|---|---|
| 是否回傳 Promise | 不回傳 Promise | 回傳 Promise |
| 是否支援 await | 不支援 | 可搭配 await |
| 錯誤處理 | 透過 onError callback 處理 | 可以用 try/catch 捕捉錯誤 |
| 是否能獲取 mutation 的回應值 | 需透過 onSuccess callback 取得 | 可直接 const result = await mutateAsync() |
| 適合場景 | 簡單的事件處理,例如按鈕點擊後發送請求 | 需要等待 API 完成後執行後續邏輯 |
<script setup>
import { useMutation } from '@tanstack/vue-query'
import axios from 'axios'
const createTodo = async (newTodo) => {
const { data } = await axios.post("http://localhost:3002/todos", newTodo);
return data;
};
const { mutate, mutateAsync, isPending } = useMutation({
mutationFn: createTodo
})
// 使用 mutate (callback 方式)
const handleAddTodoWithMutate = () => {
mutate({ title: '使用 mutate' }, {
onSuccess: (data) => {
console.log('mutate 成功:', data)
},
onError: (error) => {
console.error('mutate 失敗:', error)
}
})
}
// 使用 mutateAsync (await 方式)
const handleAddTodoWithMutateAsync = async () => {
try {
const newTodo = await mutateAsync({ title: '使用 mutateAsync' })
console.log('mutateAsync 成功:', newTodo)
} catch (error) {
console.error('mutateAsync 失敗:', error)
}
}
</script>
<template>
<div>
<button @click="handleAddTodoWithMutate" :disabled="isPending">
使用 mutate
</button>
<button @click="handleAddTodoWithMutateAsync" :disabled="isPending">
使用 mutateAsync
</button>
</div>
</template>