測驗:TanStack Query 進階 – useMutation 與樂觀更新
共 5 題,點選答案後會立即顯示結果
1. 在 TanStack Query 中,Query 和 Mutation 分別對應什麼類型的操作?
2. useMutation 的回調函數執行順序是什麼?
3. 當 mutation 操作成功後,要讓畫面上的資料保持最新,最簡單的方式是使用哪個方法?
4. 樂觀更新(Optimistic Updates)的主要特點是什麼?
5. 在實作樂觀更新時,onMutate 回調函數需要回傳什麼?
onMutate: async (todoId) => {
await queryClient.cancelQueries({ queryKey: [‘todos’] });
const previousTodos = queryClient.getQueryData([‘todos’]);
queryClient.setQueryData([‘todos’], (old) =>
old.filter(todo => todo.id !== todoId)
);
return { previousTodos }; // 這裡回傳什麼?
}
前言
在上一篇文章中,我們學會了使用 useQuery 來取得資料。但實際的應用程式不只需要「讀取」資料,還需要「新增」、「更新」、「刪除」資料。這時候就需要使用 useMutation。
本篇將帶你學會:
- 使用
useMutation處理資料變更操作 - 在操作成功後自動重新取得資料
- 實作樂觀更新,讓使用者體驗更流暢
useMutation 基本用法
什麼是 Mutation?
在 TanStack Query 的世界裡:
- Query = 讀取資料(GET 請求)
- Mutation = 變更資料(POST、PUT、DELETE 請求)
最小範例:新增 Todo
import { useMutation, useQueryClient } from '@tanstack/react-query';
function AddTodoForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newTodo: { title: string }) => {
return fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
}).then(res => res.json());
},
onSuccess: () => {
// 新增成功後,重新取得 todos 列表
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate({ title: '買牛奶' });
};
return (
<form onSubmit={handleSubmit}>
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? '新增中...' : '新增 Todo'}
</button>
</form>
);
}
Code language: JavaScript (javascript)讀懂 useMutation 的回傳值
const mutation = useMutation({ mutationFn: ... });
// 常用屬性
mutation.mutate(variables); // 執行 mutation
mutation.isPending; // 是否正在執行
mutation.isSuccess; // 是否成功
mutation.isError; // 是否失敗
mutation.error; // 錯誤物件
mutation.data; // 成功回傳的資料
Code language: JavaScript (javascript)useMutation 的回調函數
useMutation 提供四個重要的回調函數:
const mutation = useMutation({
mutationFn: async (newTodo) => {
const res = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
});
return res.json();
},
// mutation 執行前
onMutate: (variables) => {
console.log('準備新增:', variables);
},
// mutation 成功時
onSuccess: (data, variables) => {
console.log('新增成功:', data);
},
// mutation 失敗時
onError: (error, variables) => {
console.log('新增失敗:', error);
},
// mutation 結束時(不論成功或失敗)
onSettled: (data, error, variables) => {
console.log('操作結束');
},
});
Code language: JavaScript (javascript)回調執行順序
onMutate → mutationFn → onSuccess/onError → onSettled
使用 invalidateQueries 同步資料
當我們新增、更新或刪除資料後,需要讓畫面上顯示的資料保持最新。最簡單的方式是使用 invalidateQueries:
import { useQueryClient } from '@tanstack/react-query';
function TodoActions() {
const queryClient = useQueryClient();
const deleteMutation = useMutation({
mutationFn: (todoId: number) =>
fetch(`/api/todos/${todoId}`, { method: 'DELETE' }),
onSuccess: () => {
// 刪除成功後,讓 todos 的快取失效
// TanStack Query 會自動重新 fetch
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<button onClick={() => deleteMutation.mutate(1)}>
刪除 Todo #1
</button>
);
}
Code language: JavaScript (javascript)invalidateQueries 的運作原理
使用者點擊刪除 → mutation 執行 → API 回應成功
→ invalidateQueries 標記快取為過期 → useQuery 自動重新 fetch → UI 更新
這種方式簡單可靠,但有一個小缺點:使用者需要等待 API 回應後才能看到變化。
樂觀更新(Optimistic Updates)
什麼是樂觀更新?
樂觀更新是一種「先更新 UI,再等 API 回應」的策略:
傳統做法:點擊 → 等待 API → 更新 UI(使用者等待)
樂觀更新:點擊 → 立即更新 UI → 等待 API 確認(使用者不等待)
如果 API 失敗,就把 UI 回復到原本的狀態。
實作樂觀更新:刪除 Todo
function TodoList() {
const queryClient = useQueryClient();
const deleteMutation = useMutation({
mutationFn: (todoId: number) =>
fetch(`/api/todos/${todoId}`, { method: 'DELETE' }),
// 步驟 1:在 mutation 執行前,先樂觀更新 UI
onMutate: async (todoId) => {
// 取消正在進行的 queries,避免覆蓋我們的樂觀更新
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 保存目前的資料,以便失敗時回復
const previousTodos = queryClient.getQueryData(['todos']);
// 樂觀更新:立即從快取中移除被刪除的 todo
queryClient.setQueryData(['todos'], (old: Todo[]) =>
old.filter(todo => todo.id !== todoId)
);
// 回傳舊資料,供 onError 使用
return { previousTodos };
},
// 步驟 2:如果 mutation 失敗,回復到原本的狀態
onError: (err, todoId, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos);
},
// 步驟 3:不論成功或失敗,都重新取得最新資料確保同步
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
// ...
}
Code language: JavaScript (javascript)樂觀更新流程圖解
使用者點擊刪除
│
▼
onMutate 執行
├── 取消進行中的 queries
├── 保存舊資料 (previousTodos)
└── 立即更新 UI(移除該 todo)
│
▼
mutationFn 執行(呼叫 API)
│
├── 成功 → onSuccess → onSettled → invalidateQueries
│
└── 失敗 → onError(回復 previousTodos)→ onSettled
完整範例:Todo App CRUD
讓我們把學到的知識整合成一個完整的範例:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
interface Todo {
id: number;
title: string;
completed: boolean;
}
// API 函數
const api = {
getTodos: (): Promise<Todo[]> =>
fetch('/api/todos').then(res => res.json()),
addTodo: (title: string): Promise<Todo> =>
fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ title, completed: false }),
}).then(res => res.json()),
updateTodo: (todo: Todo): Promise<Todo> =>
fetch(`/api/todos/${todo.id}`, {
method: 'PUT',
body: JSON.stringify(todo),
}).then(res => res.json()),
deleteTodo: (id: number): Promise<void> =>
fetch(`/api/todos/${id}`, { method: 'DELETE' }).then(() => {}),
};
function TodoApp() {
const queryClient = useQueryClient();
// 讀取 todos
const { data: todos = [], isLoading } = useQuery({
queryKey: ['todos'],
queryFn: api.getTodos,
});
// 新增 todo
const addMutation = useMutation({
mutationFn: api.addTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
// 更新 todo(使用樂觀更新)
const updateMutation = useMutation({
mutationFn: api.updateTodo,
onMutate: async (updatedTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);
queryClient.setQueryData<Todo[]>(['todos'], (old) =>
old?.map(todo =>
todo.id === updatedTodo.id ? updatedTodo : todo
)
);
return { previousTodos };
},
onError: (err, variables, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
// 刪除 todo(使用樂觀更新)
const deleteMutation = useMutation({
mutationFn: api.deleteTodo,
onMutate: async (todoId) => {
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previousTodos = queryClient.getQueryData<Todo[]>(['todos']);
queryClient.setQueryData<Todo[]>(['todos'], (old) =>
old?.filter(todo => todo.id !== todoId)
);
return { previousTodos };
},
onError: (err, variables, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
if (isLoading) return <div>載入中...</div>;
return (
<div>
<button onClick={() => addMutation.mutate('新的待辦事項')}>
新增 Todo
</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() =>
updateMutation.mutate({
...todo,
completed: !todo.completed,
})
}
/>
<span>{todo.title}</span>
<button onClick={() => deleteMutation.mutate(todo.id)}>
刪除
</button>
</li>
))}
</ul>
</div>
);
}
Code language: JavaScript (javascript)何時該用樂觀更新?
| 情境 | 建議 |
|---|---|
| 操作成功率高(如勾選 checkbox) | 使用樂觀更新 |
| 操作可能失敗(如需要驗證) | 不使用樂觀更新 |
| 回復操作成本高 | 不使用樂觀更新 |
| 使用者體驗優先 | 使用樂觀更新 |
本篇重點整理
- useMutation 用於處理資料變更操作(POST/PUT/DELETE)
- 四個回調函數:
onMutate→onSuccess/onError→onSettled - invalidateQueries 讓快取失效,觸發重新取得資料
- 樂觀更新的三步驟:
onMutate:保存舊資料、立即更新 UIonError:失敗時回復舊資料onSettled:重新取得最新資料
下一篇預告
在下一篇文章中,我們將學習如何使用 Zustand 管理本地狀態,以及如何將 TanStack Query 與 Zustand 整合,建立完整的狀態管理架構。
進階測驗:TanStack Query 進階 – useMutation 與樂觀更新
測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。
共 5 題,包含情境題與錯誤診斷題。
1. 你正在開發一個 Todo App,需要在使用者點擊「新增」按鈕後將新的 Todo 送到後端,並在成功後更新列表。 情境題
你應該如何設計這個功能?
2. 你的 Todo App 有一個「勾選完成」的功能,使用者勾選後需要立即看到變化,不想等待 API 回應。如果 API 失敗,要能回復原狀。 情境題
這種情況最適合使用什麼技術?
3. 你正在實作樂觀更新的刪除功能。在 onMutate 中,你需要先做什麼操作來避免快取被覆蓋? 情境題
onMutate: async (todoId) => {
// 這裡應該先做什麼?
const previousTodos = queryClient.getQueryData([‘todos’]);
queryClient.setQueryData([‘todos’], (old) =>
old.filter(todo => todo.id !== todoId)
);
return { previousTodos };
}
4. 小明實作了樂觀更新,但發現 API 失敗時資料沒有回復。請看以下程式碼,問題出在哪裡? 錯誤診斷
const deleteMutation = useMutation({
mutationFn: (todoId) => fetch(`/api/todos/${todoId}`, { method: ‘DELETE’ }),
onMutate: async (todoId) => {
await queryClient.cancelQueries({ queryKey: [‘todos’] });
const previousTodos = queryClient.getQueryData([‘todos’]);
queryClient.setQueryData([‘todos’], (old) =>
old.filter(todo => todo.id !== todoId)
);
// 忘記 return { previousTodos }
},
onError: (err, todoId, context) => {
queryClient.setQueryData([‘todos’], context?.previousTodos);
},
});
5. 小華的新增 Todo 功能可以成功呼叫 API,但畫面上的列表沒有更新。請看以下程式碼,問題最可能是什麼? 錯誤診斷
const addMutation = useMutation({
mutationFn: (newTodo) => {
return fetch(‘/api/todos’, {
method: ‘POST’,
body: JSON.stringify(newTodo),
}).then(res => res.json());
},
// 沒有任何回調函數
});
// 使用
addMutation.mutate({ title: ‘買牛奶’ });