Appearance
useMutation数据变更
概述
useMutation是TanStack Query中用于处理数据变更操作的Hook,如创建、更新、删除等。它提供了加载状态管理、错误处理、乐观更新、自动失效等功能,是处理数据变更的最佳实践。
基础用法
创建Mutation
jsx
import { useMutation } from '@tanstack/react-query';
async function createUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error('Failed to create user');
}
return response.json();
}
function CreateUserForm() {
const {
mutate, // 触发mutation的函数
mutateAsync, // 异步版本
data, // mutation返回的数据
error, // 错误对象
isLoading, // 加载状态(旧版API)
isPending, // 加载状态(新版API)
isError, // 是否有错误
isSuccess, // 是否成功
reset, // 重置状态
} = useMutation({
mutationFn: createUser,
});
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
mutate({
name: formData.get('name'),
email: formData.get('email'),
});
};
return (
<form onSubmit={handleSubmit}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create User'}
</button>
{isError && <div className="error">{error.message}</div>}
{isSuccess && <div className="success">User created: {data.name}</div>}
</form>
);
}mutate vs mutateAsync
jsx
function MutateComparison() {
const { mutate, mutateAsync } = useMutation({
mutationFn: createUser,
});
// mutate: 不返回Promise
const handleWithMutate = () => {
mutate(
{ name: 'John' },
{
onSuccess: (data) => console.log('Success:', data),
onError: (error) => console.error('Error:', error),
}
);
};
// mutateAsync: 返回Promise
const handleWithMutateAsync = async () => {
try {
const data = await mutateAsync({ name: 'John' });
console.log('Success:', data);
// 可以继续执行其他异步操作
await doSomethingElse(data);
} catch (error) {
console.error('Error:', error);
}
};
return (
<div>
<button onClick={handleWithMutate}>Use mutate</button>
<button onClick={handleWithMutateAsync}>Use mutateAsync</button>
</div>
);
}CRUD操作
Create (创建)
jsx
function CreateTodo() {
const queryClient = useQueryClient();
const { mutate, isPending } = useMutation({
mutationFn: (newTodo) => {
return fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
}).then(res => res.json());
},
onSuccess: () => {
// 使todos查询失效
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
const handleSubmit = (e) => {
e.preventDefault();
mutate({
text: e.target.text.value,
completed: false,
});
e.target.reset();
};
return (
<form onSubmit={handleSubmit}>
<input name="text" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Adding...' : 'Add Todo'}
</button>
</form>
);
}Update (更新)
jsx
function UpdateTodo({ todoId }) {
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: ({ id, updates }) => {
return fetch(`/api/todos/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
}).then(res => res.json());
},
onSuccess: (data, variables) => {
// 更新单个todo的缓存
queryClient.setQueryData(['todo', variables.id], data);
// 使todos列表失效
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
const handleToggle = (todo) => {
mutate({
id: todo.id,
updates: { completed: !todo.completed },
});
};
return (
<button onClick={() => handleToggle(todo)}>
{todo.completed ? 'Undo' : 'Complete'}
</button>
);
}Delete (删除)
jsx
function DeleteTodo({ todoId }) {
const queryClient = useQueryClient();
const { mutate, isPending } = useMutation({
mutationFn: (id) => {
return fetch(`/api/todos/${id}`, {
method: 'DELETE',
});
},
onSuccess: (data, deletedId) => {
// 从缓存中移除
queryClient.removeQueries({ queryKey: ['todo', deletedId] });
// 使列表失效
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
const handleDelete = () => {
if (confirm('Are you sure?')) {
mutate(todoId);
}
};
return (
<button onClick={handleDelete} disabled={isPending}>
{isPending ? 'Deleting...' : 'Delete'}
</button>
);
}乐观更新
基础乐观更新
jsx
function OptimisticTodo() {
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// 取消相关查询
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 保存之前的数据用于回滚
const previousTodos = queryClient.getQueryData(['todos']);
// 乐观更新
queryClient.setQueryData(['todos'], (old) => {
return old.map(todo =>
todo.id === newTodo.id ? { ...todo, ...newTodo } : todo
);
});
// 返回context用于onError
return { previousTodos };
},
onError: (err, newTodo, context) => {
// 回滚
queryClient.setQueryData(['todos'], context.previousTodos);
toast.error('Failed to update todo');
},
onSettled: () => {
// 重新获取以确保同步
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
const handleToggle = (todo) => {
mutate({ ...todo, completed: !todo.completed });
};
return <TodoList onToggle={handleToggle} />;
}复杂乐观更新
jsx
function AdvancedOptimistic() {
const queryClient = useQueryClient();
const { mutate: likePo st } = useMutation({
mutationFn: (postId) => {
return fetch(`/api/posts/${postId}/like`, {
method: 'POST',
}).then(res => res.json());
},
onMutate: async (postId) => {
await queryClient.cancelQueries({ queryKey: ['posts'] });
await queryClient.cancelQueries({ queryKey: ['post', postId] });
const previousPosts = queryClient.getQueryData(['posts']);
const previousPost = queryClient.getQueryData(['post', postId]);
// 更新列表中的post
queryClient.setQueryData(['posts'], (old) =>
old?.map(post =>
post.id === postId
? {
...post,
liked: true,
likeCount: post.likeCount + 1,
}
: post
)
);
// 更新单个post
queryClient.setQueryData(['post', postId], (old) => ({
...old,
liked: true,
likeCount: old.likeCount + 1,
}));
return { previousPosts, previousPost };
},
onError: (err, postId, context) => {
queryClient.setQueryData(['posts'], context.previousPosts);
queryClient.setQueryData(['post', postId], context.previousPost);
},
onSettled: (data, error, postId) => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
queryClient.invalidateQueries({ queryKey: ['post', postId] });
},
});
return <PostList onLike={likePost} />;
}回调函数
Mutation回调
jsx
function MutationCallbacks() {
const { mutate } = useMutation({
mutationFn: createUser,
// Mutation级别的回调
onMutate: (variables) => {
console.log('Mutation starting with:', variables);
// 可以返回context给其他回调使用
return { startTime: Date.now() };
},
onSuccess: (data, variables, context) => {
console.log('Mutation succeeded:', {
data,
variables,
duration: Date.now() - context.startTime,
});
toast.success(`User ${data.name} created successfully`);
},
onError: (error, variables, context) => {
console.error('Mutation failed:', {
error,
variables,
duration: Date.now() - context.startTime,
});
toast.error(`Failed to create user: ${error.message}`);
},
onSettled: (data, error, variables, context) => {
console.log('Mutation settled:', {
data,
error,
variables,
duration: Date.now() - context.startTime,
});
},
});
// mutate调用时的回调
const handleCreate = () => {
mutate(
{ name: 'John', email: 'john@example.com' },
{
onSuccess: (data) => {
console.log('This mutate call succeeded:', data);
navigate(`/users/${data.id}`);
},
onError: (error) => {
console.error('This mutate call failed:', error);
},
}
);
};
return <button onClick={handleCreate}>Create User</button>;
}缓存失效
失效查询
jsx
import { useQueryClient } from '@tanstack/react-query';
function InvalidateQueries() {
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: updateUser,
onSuccess: (data, variables) => {
// 失效所有用户相关查询
queryClient.invalidateQueries({ queryKey: ['users'] });
// 失效特定用户
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
// 失效所有查询
queryClient.invalidateQueries();
// 精确匹配
queryClient.invalidateQueries({
queryKey: ['users'],
exact: true,
});
// 使用predicate
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === 'users' && query.queryKey[1]?.status === 'active',
});
},
});
return <UpdateUserForm onSubmit={mutate} />;
}直接更新缓存
jsx
function DirectCacheUpdate() {
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: updateUser,
onSuccess: (updatedUser) => {
// 更新单个用户缓存
queryClient.setQueryData(['user', updatedUser.id], updatedUser);
// 更新用户列表
queryClient.setQueryData(['users'], (old) =>
old?.map(user =>
user.id === updatedUser.id ? updatedUser : user
)
);
// 部分更新
queryClient.setQueryData(['user', updatedUser.id], (old) => ({
...old,
...updatedUser,
}));
},
});
return <UpdateUserForm onSubmit={mutate} />;
}批量操作
批量mutation
jsx
function BatchMutation() {
const queryClient = useQueryClient();
const { mutate, isPending } = useMutation({
mutationFn: async (userIds) => {
// 批量删除
return Promise.all(
userIds.map(id =>
fetch(`/api/users/${id}`, { method: 'DELETE' })
)
);
},
onSuccess: (data, deletedIds) => {
// 更新缓存
queryClient.setQueryData(['users'], (old) =>
old?.filter(user => !deletedIds.includes(user.id))
);
// 移除单个用户缓存
deletedIds.forEach(id => {
queryClient.removeQueries({ queryKey: ['user', id] });
});
toast.success(`Deleted ${deletedIds.length} users`);
},
});
const [selectedIds, setSelectedIds] = useState([]);
const handleBatchDelete = () => {
if (confirm(`Delete ${selectedIds.length} users?`)) {
mutate(selectedIds);
setSelectedIds([]);
}
};
return (
<div>
<UserList
selectedIds={selectedIds}
onSelect={setSelectedIds}
/>
<button
onClick={handleBatchDelete}
disabled={selectedIds.length === 0 || isPending}
>
{isPending ? 'Deleting...' : `Delete ${selectedIds.length} users`}
</button>
</div>
);
}串行mutation
jsx
function SerialMutations() {
const { mutateAsync: createUser } = useMutation({
mutationFn: (userData) => fetch('/api/users', {
method: 'POST',
body: JSON.stringify(userData),
}).then(r => r.json()),
});
const { mutateAsync: createProfile } = useMutation({
mutationFn: ({ userId, profileData }) =>
fetch(`/api/users/${userId}/profile`, {
method: 'POST',
body: JSON.stringify(profileData),
}).then(r => r.json()),
});
const handleCreateUserWithProfile = async (userData, profileData) => {
try {
// 1. 创建用户
const user = await createUser(userData);
// 2. 创建profile
const profile = await createProfile({
userId: user.id,
profileData,
});
toast.success('User and profile created');
navigate(`/users/${user.id}`);
} catch (error) {
toast.error('Failed to create user');
}
};
return <CreateUserProfileForm onSubmit={handleCreateUserWithProfile} />;
}文件上传
简单文件上传
jsx
function FileUpload() {
const { mutate, isPending, progress } = useMutation({
mutationFn: async (file) => {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error('Upload failed');
}
return response.json();
},
onSuccess: (data) => {
toast.success(`File uploaded: ${data.url}`);
},
});
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
mutate(file);
}
};
return (
<div>
<input
type="file"
onChange={handleFileChange}
disabled={isPending}
/>
{isPending && <div>Uploading...</div>}
</div>
);
}带进度的上传
jsx
function UploadWithProgress() {
const [progress, setProgress] = useState(0);
const { mutate, isPending } = useMutation({
mutationFn: async (file) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentage = Math.round((e.loaded * 100) / e.total);
setProgress(percentage);
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error('Upload failed'));
}
});
xhr.addEventListener('error', () => {
reject(new Error('Upload failed'));
});
const formData = new FormData();
formData.append('file', file);
xhr.open('POST', '/api/upload');
xhr.send(formData);
});
},
onSuccess: () => {
setProgress(0);
toast.success('Upload completed');
},
onError: () => {
setProgress(0);
toast.error('Upload failed');
},
});
return (
<div>
<input
type="file"
onChange={(e) => mutate(e.target.files[0])}
disabled={isPending}
/>
{isPending && (
<div className="progress">
<div
className="progress-bar"
style={{ width: `${progress}%` }}
/>
<span>{progress}%</span>
</div>
)}
</div>
);
}全局Mutation配置
默认选项
jsx
import { QueryClient } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
mutations: {
// 全局mutation回调
onError: (error) => {
console.error('Mutation error:', error);
toast.error(error.message);
},
onSuccess: (data) => {
console.log('Mutation success:', data);
},
// 重试配置
retry: 1,
retryDelay: 1000,
},
},
});Mutation缓存
jsx
function MutationCache() {
const queryClient = useQueryClient();
// 访问mutation缓存
const mutationCache = queryClient.getMutationCache();
// 获取所有mutations
const allMutations = mutationCache.getAll();
// 查找特定mutation
const uploadMutations = mutationCache.find({
mutationKey: ['upload'],
});
// 订阅mutation更新
useEffect(() => {
const unsubscribe = mutationCache.subscribe((event) => {
if (event.type === 'added') {
console.log('Mutation added:', event.mutation);
}
if (event.type === 'updated') {
console.log('Mutation updated:', event.mutation);
}
});
return unsubscribe;
}, [mutationCache]);
return <div>Mutations: {allMutations.length}</div>;
}总结
useMutation核心特性:
- 基础用法:mutate、mutateAsync触发变更
- CRUD操作:Create、Update、Delete封装
- 乐观更新:onMutate、回滚机制
- 回调函数:onSuccess、onError、onSettled
- 缓存失效:invalidateQueries、直接更新
- 批量操作:批量mutation、串行mutation
- 文件上传:FormData、上传进度
- 全局配置:默认选项、mutation缓存
useMutation提供了完整的数据变更解决方案,简化了状态管理和缓存更新。
第四部分:useMutation高级模式
4.1 Mutation队列和并发控制
jsx
// 1. Mutation队列管理
class MutationQueue {
constructor() {
this.queue = [];
this.isProcessing = false;
}
async add(mutationFn) {
return new Promise((resolve, reject) => {
this.queue.push({ mutationFn, resolve, reject });
this.process();
});
}
async process() {
if (this.isProcessing || this.queue.length === 0) return;
this.isProcessing = true;
while (this.queue.length > 0) {
const { mutationFn, resolve, reject } = this.queue.shift();
try {
const result = await mutationFn();
resolve(result);
} catch (error) {
reject(error);
}
}
this.isProcessing = false;
}
}
const mutationQueue = new MutationQueue();
function useQueuedMutation(mutationFn, options = {}) {
return useMutation({
mutationFn: (variables) => mutationQueue.add(() => mutationFn(variables)),
...options
});
}
// 2. 并发限制Mutation
function useConcurrentMutation(mutationFn, { maxConcurrent = 3, ...options } = {}) {
const [activeCount, setActiveCount] = useState(0);
const [pendingQueue, setPendingQueue] = useState([]);
const processPending = useCallback(async () => {
if (activeCount >= maxConcurrent || pendingQueue.length === 0) return;
const next = pendingQueue[0];
setPendingQueue(q => q.slice(1));
setActiveCount(c => c + 1);
try {
const result = await mutationFn(next.variables);
next.resolve(result);
} catch (error) {
next.reject(error);
} finally {
setActiveCount(c => c - 1);
}
}, [activeCount, maxConcurrent, pendingQueue, mutationFn]);
useEffect(() => {
processPending();
}, [activeCount, pendingQueue, processPending]);
const mutate = useCallback((variables) => {
return new Promise((resolve, reject) => {
setPendingQueue(q => [...q, { variables, resolve, reject }]);
});
}, []);
return { mutate, activeCount, queueLength: pendingQueue.length };
}
// 3. 防抖Mutation
function useDebouncedMutation(mutationFn, delay = 500, options = {}) {
const timeoutRef = useRef(null);
const latestVariablesRef = useRef(null);
const mutation = useMutation({
mutationFn,
...options
});
const debouncedMutate = useCallback((variables) => {
latestVariablesRef.current = variables;
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
mutation.mutate(latestVariablesRef.current);
}, delay);
}, [mutation, delay]);
const cancel = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
}, []);
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return {
...mutation,
mutate: debouncedMutate,
cancel
};
}
// 4. 节流Mutation
function useThrottledMutation(mutationFn, delay = 500, options = {}) {
const lastRunRef = useRef(0);
const pendingRef = useRef(null);
const mutation = useMutation({
mutationFn,
...options
});
const throttledMutate = useCallback((variables) => {
const now = Date.now();
const timeSinceLastRun = now - lastRunRef.current;
if (timeSinceLastRun >= delay) {
lastRunRef.current = now;
mutation.mutate(variables);
} else {
pendingRef.current = variables;
setTimeout(() => {
if (pendingRef.current) {
lastRunRef.current = Date.now();
mutation.mutate(pendingRef.current);
pendingRef.current = null;
}
}, delay - timeSinceLastRun);
}
}, [mutation, delay]);
return {
...mutation,
mutate: throttledMutate
};
}4.2 复杂乐观更新模式
jsx
// 1. 多级乐观更新
function useNestedOptimisticUpdate() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateComment,
onMutate: async (newComment) => {
// 取消相关查询
await queryClient.cancelQueries({ queryKey: ['posts'] });
await queryClient.cancelQueries({ queryKey: ['comments', newComment.postId] });
// 保存快照
const previousPosts = queryClient.getQueryData(['posts']);
const previousComments = queryClient.getQueryData(['comments', newComment.postId]);
// 更新评论列表
queryClient.setQueryData(['comments', newComment.postId], (old) => {
return old?.map(comment =>
comment.id === newComment.id
? { ...comment, ...newComment }
: comment
);
});
// 更新帖子中的评论计数
queryClient.setQueryData(['posts'], (old) => {
return old?.map(post =>
post.id === newComment.postId
? { ...post, commentsCount: post.commentsCount + 1 }
: post
);
});
return { previousPosts, previousComments };
},
onError: (err, newComment, context) => {
// 回滚所有更新
queryClient.setQueryData(['posts'], context.previousPosts);
queryClient.setQueryData(
['comments', newComment.postId],
context.previousComments
);
},
onSettled: (data, error, variables) => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
queryClient.invalidateQueries({ queryKey: ['comments', variables.postId] });
}
});
}
// 2. 条件乐观更新
function useConditionalOptimisticUpdate() {
const queryClient = useQueryClient();
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// 只在在线时进行乐观更新
if (!isOnline) return;
await queryClient.cancelQueries({ queryKey: ['todos'] });
const previousTodos = queryClient.getQueryData(['todos']);
queryClient.setQueryData(['todos'], (old) =>
old?.map(todo => todo.id === newTodo.id ? newTodo : todo)
);
return { previousTodos };
},
onError: (err, newTodo, context) => {
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos);
}
},
onSettled: () => {
if (isOnline) {
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
}
});
}
// 3. 带有撤销的乐观更新
function useOptimisticUpdateWithUndo() {
const queryClient = useQueryClient();
const [undoStack, setUndoStack] = useState([]);
const mutation = useMutation({
mutationFn: updateItem,
onMutate: async (newItem) => {
await queryClient.cancelQueries({ queryKey: ['items'] });
const previousItems = queryClient.getQueryData(['items']);
queryClient.setQueryData(['items'], (old) =>
old?.map(item => item.id === newItem.id ? newItem : item)
);
// 添加到撤销栈
const undoEntry = {
id: Date.now(),
previousItems,
newItem
};
setUndoStack(stack => [...stack, undoEntry]);
return { previousItems, undoId: undoEntry.id };
},
onError: (err, newItem, context) => {
queryClient.setQueryData(['items'], context.previousItems);
setUndoStack(stack => stack.filter(entry => entry.id !== context.undoId));
}
});
const undo = useCallback((undoId) => {
const entry = undoStack.find(e => e.id === undoId);
if (entry) {
queryClient.setQueryData(['items'], entry.previousItems);
setUndoStack(stack => stack.filter(e => e.id !== undoId));
}
}, [undoStack, queryClient]);
return {
...mutation,
undo,
undoStack
};
}
// 4. 乐观更新冲突解决
function useOptimisticUpdateWithConflictResolution() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateDocument,
onMutate: async (newDoc) => {
await queryClient.cancelQueries({ queryKey: ['document', newDoc.id] });
const previousDoc = queryClient.getQueryData(['document', newDoc.id]);
// 检查版本冲突
if (previousDoc && previousDoc.version !== newDoc.baseVersion) {
throw new Error('Version conflict detected');
}
// 乐观更新
queryClient.setQueryData(['document', newDoc.id], {
...previousDoc,
...newDoc,
version: previousDoc.version + 1,
isPending: true
});
return { previousDoc };
},
onSuccess: (data, variables, context) => {
// 用服务器返回的数据替换乐观更新
queryClient.setQueryData(['document', variables.id], data);
},
onError: (err, newDoc, context) => {
if (err.message === 'Version conflict detected') {
// 冲突处理:合并变更
const serverDoc = queryClient.getQueryData(['document', newDoc.id]);
const mergedDoc = mergeDocuments(context.previousDoc, newDoc, serverDoc);
queryClient.setQueryData(['document', newDoc.id], mergedDoc);
} else {
queryClient.setQueryData(['document', newDoc.id], context.previousDoc);
}
}
});
}
function mergeDocuments(base, local, server) {
// 简单的三向合并逻辑
return {
...server,
// 保留本地未冲突的更改
...Object.keys(local).reduce((acc, key) => {
if (local[key] !== base[key] && server[key] === base[key]) {
acc[key] = local[key];
}
return acc;
}, {})
};
}4.3 Mutation重试策略
jsx
// 1. 自定义重试逻辑
function useRetryableMutation(mutationFn, options = {}) {
return useMutation({
mutationFn,
retry: (failureCount, error) => {
// 网络错误:重试
if (error.message === 'Network Error') {
return failureCount < 3;
}
// 429 Too Many Requests:延迟重试
if (error.status === 429) {
return failureCount < 5;
}
// 500 服务器错误:重试
if (error.status >= 500) {
return failureCount < 2;
}
// 其他错误:不重试
return false;
},
retryDelay: (attemptIndex, error) => {
// 429错误:使用Retry-After头
if (error.status === 429) {
const retryAfter = error.headers?.['retry-after'];
return retryAfter ? parseInt(retryAfter) * 1000 : 1000 * attemptIndex;
}
// 指数退避
return Math.min(1000 * 2 ** attemptIndex, 30000);
},
...options
});
}
// 2. 带有指数退避的重试
function useExponentialBackoffMutation(mutationFn, options = {}) {
const [retryState, setRetryState] = useState({
count: 0,
lastError: null
});
const mutation = useMutation({
mutationFn: async (variables) => {
try {
const result = await mutationFn(variables);
setRetryState({ count: 0, lastError: null });
return result;
} catch (error) {
setRetryState(state => ({
count: state.count + 1,
lastError: error
}));
throw error;
}
},
retry: 5,
retryDelay: (attemptIndex) => {
const baseDelay = 1000;
const maxDelay = 60000;
const jitter = Math.random() * 1000;
return Math.min(baseDelay * Math.pow(2, attemptIndex) + jitter, maxDelay);
},
...options
});
return {
...mutation,
retryState
};
}
// 3. 智能重试(基于错误类型)
function useSmartRetryMutation(mutationFn, options = {}) {
const networkStatus = useNetworkStatus();
return useMutation({
mutationFn,
retry: (failureCount, error) => {
// 离线时暂停重试
if (!networkStatus.online) {
return false;
}
// 客户端错误不重试
if (error.status >= 400 && error.status < 500) {
return false;
}
// 服务器错误重试
if (error.status >= 500 || error.message === 'Network Error') {
return failureCount < 3;
}
return false;
},
networkMode: 'online',
...options
});
}
function useNetworkStatus() {
const [online, setOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setOnline(true);
const handleOffline = () => setOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return { online };
}4.4 Mutation持久化
jsx
// 1. 离线Mutation队列
class OfflineMutationQueue {
constructor() {
this.queue = this.loadQueue();
}
loadQueue() {
const saved = localStorage.getItem('offline-mutations');
return saved ? JSON.parse(saved) : [];
}
saveQueue() {
localStorage.setItem('offline-mutations', JSON.stringify(this.queue));
}
add(mutation) {
this.queue.push({
id: Date.now(),
...mutation,
timestamp: new Date().toISOString()
});
this.saveQueue();
}
remove(id) {
this.queue = this.queue.filter(m => m.id !== id);
this.saveQueue();
}
getAll() {
return this.queue;
}
clear() {
this.queue = [];
this.saveQueue();
}
}
const offlineQueue = new OfflineMutationQueue();
function useOfflineMutation(mutationFn, options = {}) {
const [isOnline, setIsOnline] = useState(navigator.onLine);
const queryClient = useQueryClient();
useEffect(() => {
const handleOnline = () => {
setIsOnline(true);
// 处理离线队列
processOfflineQueue();
};
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
const processOfflineQueue = async () => {
const queue = offlineQueue.getAll();
for (const mutation of queue) {
try {
await mutationFn(mutation.variables);
offlineQueue.remove(mutation.id);
if (mutation.queryKey) {
queryClient.invalidateQueries({ queryKey: mutation.queryKey });
}
} catch (error) {
console.error('Failed to process offline mutation:', error);
}
}
};
return useMutation({
mutationFn: async (variables) => {
if (!isOnline) {
// 离线时添加到队列
offlineQueue.add({
variables,
queryKey: options.queryKey
});
// 执行乐观更新
if (options.onMutate) {
options.onMutate(variables);
}
return { offline: true };
}
return mutationFn(variables);
},
...options
});
}
// 2. Mutation状态持久化
function usePersistentMutation(key, mutationFn, options = {}) {
const [persistedState, setPersistedState] = useState(() => {
const saved = localStorage.getItem(`mutation-${key}`);
return saved ? JSON.parse(saved) : null;
});
const mutation = useMutation({
mutationFn,
onMutate: (variables) => {
const state = { status: 'loading', variables };
localStorage.setItem(`mutation-${key}`, JSON.stringify(state));
setPersistedState(state);
options.onMutate?.(variables);
},
onSuccess: (data, variables) => {
const state = { status: 'success', data, variables };
localStorage.setItem(`mutation-${key}`, JSON.stringify(state));
setPersistedState(state);
options.onSuccess?.(data, variables);
},
onError: (error, variables) => {
const state = { status: 'error', error: error.message, variables };
localStorage.setItem(`mutation-${key}`, JSON.stringify(state));
setPersistedState(state);
options.onError?.(error, variables);
},
...options
});
const clearPersistedState = useCallback(() => {
localStorage.removeItem(`mutation-${key}`);
setPersistedState(null);
}, [key]);
return {
...mutation,
persistedState,
clearPersistedState
};
}4.5 Mutation监控和分析
jsx
// 1. Mutation性能监控
class MutationPerformanceMonitor {
constructor() {
this.metrics = [];
}
start(mutationKey) {
return {
startTime: performance.now(),
mutationKey
};
}
end(context, status) {
const endTime = performance.now();
const duration = endTime - context.startTime;
const metric = {
mutationKey: context.mutationKey,
duration,
status,
timestamp: new Date().toISOString()
};
this.metrics.push(metric);
// 发送到分析服务
if (duration > 3000) {
console.warn('Slow mutation detected:', metric);
}
return metric;
}
getMetrics() {
return this.metrics;
}
getAverageDuration(mutationKey) {
const filtered = this.metrics.filter(m => m.mutationKey === mutationKey);
if (filtered.length === 0) return 0;
const sum = filtered.reduce((acc, m) => acc + m.duration, 0);
return sum / filtered.length;
}
}
const mutationMonitor = new MutationPerformanceMonitor();
function useMonitoredMutation(mutationKey, mutationFn, options = {}) {
return useMutation({
mutationFn: async (variables) => {
const context = mutationMonitor.start(mutationKey);
try {
const result = await mutationFn(variables);
mutationMonitor.end(context, 'success');
return result;
} catch (error) {
mutationMonitor.end(context, 'error');
throw error;
}
},
...options
});
}
// 2. Mutation错误追踪
class MutationErrorTracker {
constructor() {
this.errors = [];
}
track(mutationKey, error, variables) {
const errorEntry = {
mutationKey,
error: {
message: error.message,
stack: error.stack,
status: error.status
},
variables,
timestamp: new Date().toISOString()
};
this.errors.push(errorEntry);
// 发送到错误追踪服务
this.reportError(errorEntry);
}
reportError(errorEntry) {
// 发送到Sentry, LogRocket等
console.error('Mutation Error:', errorEntry);
}
getErrors() {
return this.errors;
}
getErrorRate(mutationKey) {
const total = this.errors.filter(e => e.mutationKey === mutationKey).length;
return total;
}
}
const errorTracker = new MutationErrorTracker();
function useTrackedMutation(mutationKey, mutationFn, options = {}) {
return useMutation({
mutationFn,
onError: (error, variables) => {
errorTracker.track(mutationKey, error, variables);
options.onError?.(error, variables);
},
...options
});
}useMutation最佳实践总结
1. 并发控制
✅ 实现Mutation队列
✅ 限制并发数量
✅ 使用防抖/节流
2. 乐观更新
✅ 多级更新处理
✅ 冲突解决机制
✅ 撤销/重做功能
3. 重试策略
✅ 智能重试逻辑
✅ 指数退避算法
✅ 基于错误类型重试
4. 离线支持
✅ 持久化Mutation
✅ 离线队列管理
✅ 在线时同步
5. 监控分析
✅ 性能追踪
✅ 错误监控
✅ 指标收集useMutation是数据变更的强大工具,合理使用可以构建健壮的数据交互层。