效果

无需用户等待服务器响应,立即更新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()

// 查询当前的todo列表
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 在 mutationFn 之前执行
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>
)
}

对比

  1. 常规请求流程:
    • 用户提交新数据(POST 请求)
    • 等待服务器响应
    • 收到成功响应后,重新获取列表数据(GET 请求)
    • 更新 UI
  2. 乐观更新流程:
    • 用户提交新数据
    • 在发送 POST 请求之前,立即更新 UI
    • 发送 POST 请求
    • 如果成功,用户不会注意到任何变化
    • 如果失败,回滚到之前的状态

补充说明:

  1. 拦截处理:
    useMutation 不是真的”拦截”POST 请求,而是提供了一个在请求发出前执行操作的机会(通过 onMutate 回调)。
  2. 数据合并:
    合并新旧数据通常在 onMutate 回调中使用 queryClient.setQueryData 完成。
  3. 缓存机制:
    TanStack Query 确实提供了强大的缓存机制,这使得存储之前的状态和进行回滚变得简单。
  4. 最终同步:
    即使乐观更新成功,通常还是会在 onSettled 回调中重新获取数据,以确保客户端数据与服务器完全同步。
  5. 性能提升:
    乐观更新的主要优势是提高了应用的感知性能,让用户感觉操作是即时的。

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>
)
}