乐观更新
Solid Query 提供两种方式,让你在修改(mutation)完成前就乐观地更新 UI。你可以使用 onMutate 选项直接更新缓存,也可以利用返回的 variables,基于 useMutation 的结果来更新 UI。
通过 UI 实现
这是较简单的方式,不直接与缓存交互。
1const addTodoMutation = useMutation(() => ({2 mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),3 // make sure to _return_ the Promise from the query invalidation4 // so that the mutation stays in `pending` state until the refetch is finished5 onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),6}))随后你将能访问 addTodoMutation.variables,其中包含新添加的待办事项。在渲染该查询的 UI 列表中,你可在 mutation isPending 期间向列表末尾追加一项:
1<ul>2 <For each={todoQuery.data}>{(todo) => <li>{todo.text}</li>}</For>3 <Show when={addTodoMutation.isPending}>4 <li style={{ opacity: 0.5 }}>{addTodoMutation.variables}</li>5 </Show>6</ul>只要 mutation 处于等待中(pending),我们就渲染一个临时项,并赋予其不同的 opacity。一旦 mutation 完成,该项将自动不再渲染。若重新拉取(refetch)成功,你将在列表中看到该项变为“正常项”。
若 mutation 报错,该项同样会消失。但如果你希望它继续显示,可检查 mutation 的 isError 状态。variables 在 mutation 出错时 不会 被清空,因此你仍可访问它们,甚至可能借此展示重试按钮:
1<Show when={addTodoMutation.isError}>2 <li style={{ color: 'red' }}>3 {addTodoMutation.variables}4 <button onClick={() => addTodoMutation.mutate(addTodoMutation.variables)}>5 Retry6 </button>7 </li>8</Show>若修改与查询不在同一组件中
当修改与查询位于同一组件时,此方法效果很好。但你也可通过专用的 useMutationState 原语,在其他组件中访问全部 mutation。它最适合与 mutationKey 结合使用:
1// somewhere in your app2const mutation = useMutation(() => ({3 mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),4 onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),5 mutationKey: ['addTodo'],6}))7 8// access variables somewhere else9const variables = useMutationState(() => ({10 filters: { mutationKey: ['addTodo'], status: 'pending' },11 select: (mutation) => mutation.state.variables,12}))variables 将是一个 Array,因为可能同时运行多个 mutation。若需为各项指定唯一键,你还可以选择 mutation.state.submittedAt。这甚至能让并发乐观更新的展示变得轻而易举。
通过缓存实现
当你在执行修改前乐观地更新状态时,该修改存在失败风险。在大多数失败场景下,你只需对相关乐观查询触发一次重新拉取(refetch),即可将其回滚至真实的服务端状态(server state)。但在某些情况下,重新拉取可能无法正确工作,且该修改错误可能代表某种服务端问题,导致无法成功重新拉取。此时,你可选择手动回滚更新。
为此,useMutation 的 onMutate 处理器选项允许你返回一个值,该值后续将作为最后一个参数,同时传入 onError 和 onSettled 处理器中。多数情况下,传入一个回滚函数最为实用。
向待办事项列表添加新待办事项时更新列表
1const queryClient = useQueryClient()2 3useMutation(() => ({4 mutationFn: updateTodo,5 // When mutate is called:6 onMutate: async (newTodo, context) => {7 // Cancel any outgoing refetches8 // (so they don't overwrite our optimistic update)9 await context.client.cancelQueries({ queryKey: ['todos'] })10 11 // Snapshot the previous value12 const previousTodos = context.client.getQueryData(['todos'])13 14 // Optimistically update to the new value15 context.client.setQueryData(['todos'], (old) => [...old, newTodo])16 17 // Return a result with the snapshotted value18 return { previousTodos }19 },20 // If the mutation fails,21 // use the result returned from onMutate to roll back22 onError: (err, newTodo, onMutateResult, context) => {23 context.client.setQueryData(['todos'], onMutateResult.previousTodos)24 },25 // Always refetch after error or success:26 onSettled: (data, error, variables, onMutateResult, context) =>27 context.client.invalidateQueries({ queryKey: ['todos'] }),28}))更新单个待办事项
1useMutation(() => ({2 mutationFn: updateTodo,3 // When mutate is called:4 onMutate: async (newTodo, context) => {5 // Cancel any outgoing refetches6 // (so they don't overwrite our optimistic update)7 await context.client.cancelQueries({ queryKey: ['todos', newTodo.id] })8 9 // Snapshot the previous value10 const previousTodo = context.client.getQueryData(['todos', newTodo.id])11 12 // Optimistically update to the new value13 context.client.setQueryData(['todos', newTodo.id], newTodo)14 15 // Return a result with the previous and new todo16 return { previousTodo, newTodo }17 },18 // If the mutation fails, use the result we returned above19 onError: (err, newTodo, onMutateResult, context) => {20 context.client.setQueryData(21 ['todos', onMutateResult.newTodo.id],22 onMutateResult.previousTodo,23 )24 },25 // Always refetch after error or success:26 onSettled: (newTodo, error, variables, onMutateResult, context) =>27 context.client.invalidateQueries({ queryKey: ['todos', newTodo.id] }),28}))你也可按需使用 onSettled 函数,替代单独的 onError 和 onSuccess 处理器:
1useMutation(() => ({2 mutationFn: updateTodo,3 // ...4 onSettled: async (newTodo, error, variables, onMutateResult, context) => {5 if (error) {6 // do something7 }8 },9}))何时选用哪种方式
若仅有一个位置需要展示乐观结果,则直接使用 variables 并更新 UI 是代码量更少、逻辑更清晰的方法。例如,你完全无需处理回滚。
然而,若屏幕上有多处位置需感知该更新,则直接操作缓存将自动为你处理所有这些位置。
延伸阅读
请参阅 TkDodo 撰写的指南:并发乐观更新。