修改
与查询不同,修改通常用于创建/更新/删除数据或执行服务端副作用(server side-effects)。为此,TanStack Query 导出了一个 useMutation 原语。
以下是一个将新待办事项添加到服务端的修改示例:
1import { Switch, Match, Show } from 'solid-js'2 3function App() {4 const mutation = useMutation(() => ({5 mutationFn: (newTodo) => {6 return axios.post('/todos', newTodo)7 },8 }))9 10 return (11 <div>12 <Switch>13 <Match when={mutation.isPending}>Adding todo...</Match>14 <Match when={true}>15 <Show when={mutation.isError}>16 <div>An error occurred: {mutation.error.message}</div>17 </Show>1819 <Show when={mutation.isSuccess}>20 <div>Todo added!</div>21 </Show>2223 <button24 onClick={() => {25 mutation.mutate({ id: new Date(), title: 'Do Laundry' })26 }}27 >28 Create Todo29 </button>30 </Match>31 </Switch>32 </div>33 )34}在任意时刻,一个修改只能处于以下状态之一:
isIdle或status === 'idle'— 修改当前处于空闲或全新/重置状态isPending或status === 'pending'— 修改当前正在运行isError或status === 'error'— 修改遇到了错误isSuccess或status === 'success'— 修改成功完成,且修改数据可用
除这些主要状态外,根据修改所处的具体状态,还可获得更多信息:
error— 若修改处于error状态,则可通过error属性获取错误信息。data— 若修改处于success状态,则可通过data属性获取数据。
在上述示例中,你还看到:可通过向 mutate 函数传入单个变量或对象,将变量传递给修改函数。
即使仅使用变量,修改本身也并不特别;但当与 onSuccess 选项、Query Client 的 invalidateQueries 方法 和 Query Client 的 setQueryData 方法 结合使用时,修改便成为一种非常强大的工具。
1const CreateTodo = () => {2 const mutation = useMutation(() => ({3 mutationFn: (formData) => {4 return fetch('/api', formData)5 },6 }))7 const onSubmit = (event) => {8 event.preventDefault()9 mutation.mutate(new FormData(event.target))10 }11 12 return <form onSubmit={onSubmit}>...</form>13}重置修改状态
有时你需要清除某个修改请求的 error 或 data。为此,可使用 reset 函数来处理:
1import { createSignal, Show } from 'solid-js'2 3const CreateTodo = () => {4 const [title, setTitle] = createSignal('')5 const mutation = useMutation(() => ({ mutationFn: createTodo }))6 7 const onCreateTodo = (e) => {8 e.preventDefault()9 mutation.mutate({ title: title() })10 }11 12 return (13 <form onSubmit={onCreateTodo}>14 <Show when={mutation.error}>15 <h5 onClick={() => mutation.reset()}>{mutation.error}</h5>16 </Show>17 <input18 type="text"19 value={title()}20 onInput={(e) => setTitle(e.currentTarget.value)}21 />22 <br />23 <button type="submit">Create Todo</button>24 </form>25 )26}修改副作用
useMutation 提供了一些辅助选项,允许你在修改生命周期的任何阶段快速便捷地执行副作用。这些选项既适用于 修改后使查询失效并重新拉取,也适用于 乐观更新。
1useMutation(() => ({2 mutationFn: addTodo,3 onMutate: (variables, context) => {4 // A mutation is about to happen!5 6 // Optionally return a result containing data to use when for example rolling back7 return { id: 1 }8 },9 onError: (error, variables, onMutateResult, context) => {10 // An error happened!11 console.log(`rolling back optimistic update with id ${onMutateResult.id}`)12 },13 onSuccess: (data, variables, onMutateResult, context) => {14 // Boom baby!15 },16 onSettled: (data, error, variables, onMutateResult, context) => {17 // Error or success... doesn't matter!18 },19}))若在任一回调函数中返回 Promise,则该 Promise 将先被等待(await),然后才会调用下一个回调:
1useMutation(() => ({2 mutationFn: addTodo,3 onSuccess: async () => {4 console.log("I'm first!")5 },6 onSettled: async () => {7 console.log("I'm second!")8 },9}))你可能希望在调用 mutate 时,触发超出 useMutation 上定义的回调之外的额外回调。这可用于触发组件特定的副作用。为此,你可在修改变量之后,向 mutate 函数传入任意相同类型的回调选项。支持的选项包括:onSuccess、onError 和 onSettled。请注意:若组件在修改完成前卸载,则这些额外回调不会执行。
1useMutation(() => ({2 mutationFn: addTodo,3 onSuccess: (data, variables, onMutateResult, context) => {4 // I will fire first5 },6 onError: (error, variables, onMutateResult, context) => {7 // I will fire first8 },9 onSettled: (data, error, variables, onMutateResult, context) => {10 // I will fire first11 },12}))13 14mutate(todo, {15 onSuccess: (data, variables, onMutateResult, context) => {16 // I will fire second!17 },18 onError: (error, variables, onMutateResult, context) => {19 // I will fire second!20 },21 onSettled: (data, error, variables, onMutateResult, context) => {22 // I will fire second!23 },24})连续修改
在处理连续修改时,onSuccess、onError 和 onSettled 回调的行为略有不同。当它们作为参数传给 mutate 函数时,仅会在组件仍挂载的前提下触发一次。这是因为每次调用 mutate 函数时,修改观察者(mutation observer)都会被移除并重新订阅。相反,useMutation 上定义的处理器会对每次 mutate 调用都执行。
请注意:传给
useMutation的mutationFn很可能是异步的。此时,修改实际完成的顺序可能与mutate函数调用的顺序不一致。
1useMutation(() => ({2 mutationFn: addTodo,3 onSuccess: (data, variables, onMutateResult, context) => {4 // Will be called 3 times5 },6}))7 8const todos = ['Todo 1', 'Todo 2', 'Todo 3']9todos.forEach((todo) => {10 mutate(todo, {11 onSuccess: (data, variables, onMutateResult, context) => {12 // Will execute only once, for the last mutation (Todo 3),13 // regardless which mutation resolves first14 },15 })16})Promise
使用 mutateAsync 替代 mutate,可获得一个 Promise:成功时解析(resolve),出错时抛出(throw)。例如,这可用于组合副作用。
1const mutation = useMutation(() => ({ mutationFn: addTodo }))2 3try {4 const todo = await mutation.mutateAsync(todo)5 console.log(todo)6} catch (error) {7 console.error(error)8} finally {9 console.log('done')10}重试
默认情况下,TanStack Query 不会在修改出错时重试,但可通过 retry 选项启用重试:
1const mutation = useMutation(() => ({2 mutationFn: addTodo,3 retry: 3,4}))若因设备离线导致修改失败,则设备重连后,这些修改将按原顺序重试。
持久化修改
如有需要,修改可持久化至存储并在后续恢复。这可通过注水(hydration)函数实现:
1const queryClient = new QueryClient()2 3// Define the "addTodo" mutation4queryClient.setMutationDefaults(['addTodo'], {5 mutationFn: addTodo,6 onMutate: async (variables, context) => {7 // Cancel current queries for the todos list8 await context.client.cancelQueries({ queryKey: ['todos'] })9 10 // Create optimistic todo11 const optimisticTodo = { id: uuid(), title: variables.title }12 13 // Add optimistic todo to todos list14 context.client.setQueryData(['todos'], (old) => [...old, optimisticTodo])15 16 // Return a result with the optimistic todo17 return { optimisticTodo }18 },19 onSuccess: (result, variables, onMutateResult, context) => {20 // Replace optimistic todo in the todos list with the result21 context.client.setQueryData(['todos'], (old) =>22 old.map((todo) =>23 todo.id === onMutateResult.optimisticTodo.id ? result : todo,24 ),25 )26 },27 onError: (error, variables, onMutateResult, context) => {28 // Remove optimistic todo from the todos list29 context.client.setQueryData(['todos'], (old) =>30 old.filter((todo) => todo.id !== onMutateResult.optimisticTodo.id),31 )32 },33 retry: 3,34})35 36// Start mutation in some component:37const mutation = useMutation(() => ({ mutationKey: ['addTodo'] }))38mutation.mutate({ title: 'title' })39 40// If the mutation has been paused because the device is for example offline,41// Then the paused mutation can be dehydrated when the application quits:42const state = dehydrate(queryClient)43 44// The mutation can then be hydrated again when the application is started:45hydrate(queryClient, state)46 47// Resume the paused mutations:48queryClient.resumePausedMutations()持久化离线修改
若使用 persistQueryClient 插件持久化离线修改,则页面重载后无法恢复这些修改,除非你提供默认的修改函数。
这是技术限制所致:持久化到外部存储时,仅会保存修改的状态,而函数无法序列化。注水后,触发修改的组件可能尚未挂载,因此调用 resumePausedMutations 可能导致错误:No mutationFn found。
1const persister = createAsyncStoragePersister({2 storage: window.localStorage,3})4const queryClient = new QueryClient({5 defaultOptions: {6 queries: {7 gcTime: 24 * 60 * 60 * 1000, // 24 hours8 },9 },10})11 12// we need a default mutation function so that paused mutations can resume after a page reload13queryClient.setMutationDefaults(['todos'], {14 mutationFn: ({ id, data }) => {15 return api.updateTodo(id, data)16 },17})18 19export default function App() {20 return (21 <PersistQueryClientProvider22 client={queryClient}23 persistOptions={{ persister }}24 onSuccess={() => {25 // resume mutations after initial restore from localStorage was successful26 queryClient.resumePausedMutations()27 }}28 >29 <RestOfTheApp />30 </PersistQueryClientProvider>31 )32}修改作用域
默认情况下,所有修改均并行运行 —— 即便你多次调用同一修改的 .mutate()。可通过为修改指定 scope 并配合 id 来避免这种情况。所有具有相同 scope.id 的修改将串行运行,即:当它们被触发时,若该作用域下已有修改正在运行,则新修改将进入 isPaused: true 状态;它们会被加入队列,并在其轮到执行时自动恢复。
1const mutation = useMutation(() => ({2 mutationFn: addTodo,3 scope: {4 id: 'todo',5 },6}))延伸阅读
如需了解更多关于修改的信息,请参阅 TkDodo 关于掌握 TanStack Query 中修改的博文。