效果
无需用户等待服务器响应,立即更新UI,提高用户体验。
它是一种在等待服务器响应的同时,提供即时用户反馈的技术,同时还保留了在操作失败时恢复到之前状态的能力。
常规请求
用户将新数据通过 POST 请求提交给后端,然后通过 GET 重新请求列表数据拿到最新的数据。
乐观更新请求
在 POST 请求发出去之前做一些额外处理(非拦截),将用户提交的数据和当前的列表旧数据进行合并,直接更新在 UI 当中,无需等待服务器的异步结果。如果新增请求结果是成功的则用户没有任何感知,如果失败了则通过 @tanstack/query
提供的缓存机制数据来回滚到上一次的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query'
function TodoList() { const queryClient = useQueryClient()
const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/api/todos').then(res => res.json()), })
const addTodoMutation = useMutation({ mutationFn: newTodo => { console.log("4. 发送网络请求") return fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo), }).then(res => res.json()) }, onMutate: async newTodo => { console.log("2. onMutate 开始执行,接收的参数:", newTodo) console.log("3. 取消相关查询") await queryClient.cancelQueries({ queryKey: ['todos'] })
const previousTodos = queryClient.getQueryData(['todos']) console.log("3.5 执行乐观更新") queryClient.setQueryData(['todos'], old => { const updatedTodos = [...old, { ...newTodo, id: Date.now() }] console.log("乐观更新后的数据:", updatedTodos) return updatedTodos })
return { previousTodos } }, onError: (err, newTodo, context) => { console.log("5. (如果发生错误) 回滚到之前的状态") queryClient.setQueryData(['todos'], context.previousTodos) }, onSettled: () => { console.log("6. 重新获取最新数据") queryClient.invalidateQueries({ queryKey: ['todos'] }) }, })
const addTodo = (title) => { console.log("1. 调用 addTodoMutation.mutate") addTodoMutation.mutate({ title }) }
return ( <div> {todos?.map(todo => <p key={todo.id}>{todo.title}</p>)} <button onClick={() => addTodo('New Todo')}>Add Todo</button> </div> ) }
|
对比
- 常规请求流程:
- 用户提交新数据(POST 请求)
- 等待服务器响应
- 收到成功响应后,重新获取列表数据(GET 请求)
- 更新 UI
- 乐观更新流程:
- 用户提交新数据
- 在发送 POST 请求之前,立即更新 UI
- 发送 POST 请求
- 如果成功,用户不会注意到任何变化
- 如果失败,回滚到之前的状态
补充说明:
- 拦截处理:
useMutation
不是真的”拦截”POST 请求,而是提供了一个在请求发出前执行操作的机会(通过 onMutate
回调)。 - 数据合并:
合并新旧数据通常在 onMutate
回调中使用 queryClient.setQueryData
完成。 - 缓存机制:
TanStack Query 确实提供了强大的缓存机制,这使得存储之前的状态和进行回滚变得简单。 - 最终同步:
即使乐观更新成功,通常还是会在 onSettled
回调中重新获取数据,以确保客户端数据与服务器完全同步。 - 性能提升:
乐观更新的主要优势是提高了应用的感知性能,让用户感觉操作是即时的。
fetch 请求注意
HTTP 状态是 200,但后端给的自定义状态是 4xx 或 5xx默认情况下 onError
不会被触发,因为 fetch 不会将这视为错误。
- fetch 只有在网络错误或者无法完成请求时才会 reject(例如,DNS 查找失败、服务器不可达,网络错误等)。
- 对于 HTTP 错误状态(包括 4xx 和 5xx),fetch promise 仍然会 resolve。
- 这意味着即使收到 500 错误,fetch 也不会自动抛出错误。
解决办法:手动处理 fetch 请求结果,手动向上抛出错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query'
function TodoList() { const queryClient = useQueryClient()
const addTodoMutation = useMutation({ mutationFn: async newTodo => { const response = await fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo), headers: { 'Content-Type': 'application/json', }, }); const data = await response.json(); if (data.status >= 400) { throw new Error(data.message || 'An error occurred'); } return data; }, onMutate: async newTodo => { await queryClient.cancelQueries({ queryKey: ['todos'] }); const previousTodos = queryClient.getQueryData(['todos']); queryClient.setQueryData(['todos'], old => [...old, { ...newTodo, id: Date.now() }]); return { previousTodos }; }, onError: (err, newTodo, context) => { console.log("Error occurred:", err.message); queryClient.setQueryData(['todos'], context.previousTodos); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }); }, })
const addTodo = (title) => { addTodoMutation.mutate({ title }); }
return ( <div> {/* 组件的其余部分 */} </div> ) }
|