与查询不同,变更通常用于创建/更新/删除数据或执行服务器端副作用。为此,TanStack Query 导出了一个 useMutation 钩子。
这是一个典型的变更,它向服务器添加了一个新的待办事项:
function App() {
const mutation = useMutation(() => {
mutationFn: (newTodo) => {
return axios.post('/todos', newTodo)
},
})
return (
<div>
{mutation.isPending ? (
'正在添加待办事项...'
) : (
<>
{mutation.isError ? (
<div>发生错误:{mutation.error.message}</div>
) : null}
{mutation.isSuccess ? <div>待办事项已添加!</div> : null}
<button
onClick={() => {
mutation.mutate({ id: new Date(), title: '洗衣服' })
}}
>
创建待办事项
</button>
</>
)}
</div>
)
}
function App() {
const mutation = useMutation(() => {
mutationFn: (newTodo) => {
return axios.post('/todos', newTodo)
},
})
return (
<div>
{mutation.isPending ? (
'正在添加待办事项...'
) : (
<>
{mutation.isError ? (
<div>发生错误:{mutation.error.message}</div>
) : null}
{mutation.isSuccess ? <div>待办事项已添加!</div> : null}
<button
onClick={() => {
mutation.mutate({ id: new Date(), title: '洗衣服' })
}}
>
创建待办事项
</button>
</>
)}
</div>
)
}
在任何给定时间,变更只能处于以下状态之一:
除了上述主要状态之外,根据变更的状态,还可以使用更多信息:
在上面的示例中,您还看到可以通过使用单个变量或对象调用 mutate 函数来将变量传递给变更函数。
即使只有变量,变更也不是那么特别,但是当与 onSuccess 选项、Query Client 的 invalidateQueries 方法和 Query Client 的 setQueryData 方法一起使用时,变更就变成了一个非常强大的工具。
重要提示:mutate 函数是一个异步函数,这意味着您不能在 Solid 16 及更早版本的事件回调中直接使用它。如果您需要在 onSubmit 中访问事件,则需要将 mutate 包装在另一个函数中。这是由于 Solid 事件池。
// 这在 Solid 16 及更早版���中不起作用
const CreateTodo = () => {
const mutation = useMutation(() => {
mutationFn: (event) => {
event.preventDefault()
return fetch('/api', new FormData(event.target))
},
})
return <form onSubmit={mutation.mutate}>...</form>
}
// 这将起作用
const CreateTodo = () => {
const mutation = useMutation(() => {
mutationFn: (formData) => {
return fetch('/api', formData)
},
})
const onSubmit = (event) => {
event.preventDefault()
mutation.mutate(new FormData(event.target))
}
return <form onSubmit={onSubmit}>...</form>
}
// 这在 Solid 16 及更早版本中不起作用
const CreateTodo = () => {
const mutation = useMutation(() => {
mutationFn: (event) => {
event.preventDefault()
return fetch('/api', new FormData(event.target))
},
})
return <form onSubmit={mutation.mutate}>...</form>
}
// 这将起作用
const CreateTodo = () => {
const mutation = useMutation(() => {
mutationFn: (formData) => {
return fetch('/api', formData)
},
})
const onSubmit = (event) => {
event.preventDefault()
mutation.mutate(new FormData(event.target))
}
return <form onSubmit={onSubmit}>...</form>
}
有时您需要清除变更请求的 error 或 data。为此,您可以使用 reset 函数来处理:
const CreateTodo = () => {
const [title, setTitle] = useState('')
const mutation = useMutation(() => { mutationFn: createTodo })
const onCreateTodo = (e) => {
e.preventDefault()
mutation.mutate({ title })
}
return (
<form onSubmit={onCreateTodo}>
{mutation.error && (
<h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
)}
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<br />
<button type="submit">创建待办事项</button>
</form>
)
}
const CreateTodo = () => {
const [title, setTitle] = useState('')
const mutation = useMutation(() => { mutationFn: createTodo })
const onCreateTodo = (e) => {
e.preventDefault()
mutation.mutate({ title })
}
return (
<form onSubmit={onCreateTodo}>
{mutation.error && (
<h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
)}
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<br />
<button type="submit">创建待办事项</button>
</form>
)
}
useMutation 附带一些辅助选项,允许在变更生命周期的任何阶段快速轻松地产生副作用。这些对于在变更后使查询失效并重新获取甚至乐观更新都非常方便
useMutation(() => {
mutationFn: addTodo,
onMutate: (variables) => {
// 变更即将发生!
// (可选)返回一个包含数据的上下文,以便在回滚时使用
return { id: 1 }
},
onError: (error, variables, context) => {
// 发生错误!
console.log(`正在使用 ID ${context.id} 回滚乐观更新`)
},
onSuccess: (data, variables, context) => {
// 太棒了!
},
onSettled: (data, error, variables, context) => {
// 错误或成功……都没关系!
},
})
useMutation(() => {
mutationFn: addTodo,
onMutate: (variables) => {
// 变更即将发生!
// (可选)返回一个包含数据的上下文,以便在回滚时使用
return { id: 1 }
},
onError: (error, variables, context) => {
// 发生错误!
console.log(`正在使用 ID ${context.id} 回滚乐观更新`)
},
onSuccess: (data, variables, context) => {
// 太棒了!
},
onSettled: (data, error, variables, context) => {
// 错误或成功……都没关系!
},
})
在任何回调函数中返回 Promise 时,它将首先被等待,然后才会调用下一个回调:
useMutation(() => {
mutationFn: addTodo,
onSuccess: async () => {
console.log("我是第一个!")
},
onSettled: async () => {
console.log("我是第二个!")
},
})
useMutation(() => {
mutationFn: addTodo,
onSuccess: async () => {
console.log("我是第一个!")
},
onSettled: async () => {
console.log("我是第二个!")
},
})
您可能会发现,在调用 mutate 时,您希望触发额外的回调,而不是 useMutation 上定义的回调。这可用于触发特定于组件的副作用。为此,您可以在变更变量之后向 mutate 函数提供任何相同的回调选项。支持的选项包括:onSuccess、onError 和 onSettled。请记住,如果您的组件在变更完成_之前_卸载,则这些额外的回调将不会运行。
useMutation(() => {
mutationFn: addTodo,
onSuccess: (data, variables, context) => {
// 我将首先触发
},
onError: (error, variables, context) => {
// 我将首先触发
},
onSettled: (data, error, variables, context) => {
// 我将首先触发
},
})
mutate(todo, {
onSuccess: (data, variables, context) => {
// 我将第二个触发!
},
onError: (error, variables, context) => {
// 我将第二个触发!
},
onSettled: (data, error, variables, context) => {
// 我将第二个触发!
},
})
useMutation(() => {
mutationFn: addTodo,
onSuccess: (data, variables, context) => {
// 我将首先触发
},
onError: (error, variables, context) => {
// 我将首先触发
},
onSettled: (data, error, variables, context) => {
// 我将首先触发
},
})
mutate(todo, {
onSuccess: (data, variables, context) => {
// 我将第二个触发!
},
onError: (error, variables, context) => {
// 我将第二个触发!
},
onSettled: (data, error, variables, context) => {
// 我将第二个触发!
},
})
在处理 onSuccess、onError 和 onSettled 回调时,连续变更的处理方式略有不同。当传递给 mutate 函数时,它们只会被触发_一次_,并且仅当组件仍然挂载时。这是因为每次调用 mutate 函数时,都会删除并重新订阅变更观察者。相反,useMutation 处理程序会对每次 mutate 调用执行。
请注意,传递给 useMutation 的 mutationFn 很可能是异步的。在这种情况下,变更完成的顺序可能与 mutate 函数调用的顺序不同。
useMutation(() => {
mutationFn: addTodo,
onSuccess: (data, variables, context) => {
// 将被调用 3 次
},
})
const todos = ['Todo 1', 'Todo 2', 'Todo 3']
todos.forEach((todo) => {
mutate(todo, {
onSuccess: (data, variables, context) => {
// 仅对最后一次变更(Todo 3)执行一次,
// 无论哪个变更首先解析
},
})
})
useMutation(() => {
mutationFn: addTodo,
onSuccess: (data, variables, context) => {
// 将被调用 3 次
},
})
const todos = ['Todo 1', 'Todo 2', 'Todo 3']
todos.forEach((todo) => {
mutate(todo, {
onSuccess: (data, variables, context) => {
// 仅对最后一次变更(Todo 3)执行一次,
// 无论哪个变更首先解析
},
})
})
使用 mutateAsync 代替 mutate 以获取一个 Promise,该 Promise 将在成功时解析或在错误时抛出。例如,这可用于组合副作用。
const mutation = useMutation(() => { mutationFn: addTodo })
try {
const todo = await mutation.mutateAsync(todo)
console.log(todo)
} catch (error) {
console.error(error)
} finally {
console.log('完成')
}
const mutation = useMutation(() => { mutationFn: addTodo })
try {
const todo = await mutation.mutateAsync(todo)
console.log(todo)
} catch (error) {
console.error(error)
} finally {
console.log('完成')
}
默认情况下,TanStack Query 不会因错误重试变更,但可以使用 retry 选项:
const mutation = useMutation(() => {
mutationFn: addTodo,
retry: 3,
})
const mutation = useMutation(() => {
mutationFn: addTodo,
retry: 3,
})
如果由于设备离线而导致变更失败,则在设备重新连接时将以相同的顺序重试它们。
如果需要,可以将变更持久化到存储中,并在稍后恢复。这可以通过水合函数来完成:
const queryClient = new QueryClient()
// 定义“addTodo”变更
queryClient.setMutationDefaults(['addTodo'], {
mutationFn: addTodo,
onMutate: async (variables) => {
// 取消待办事项列表的当前查询
await queryClient.cancelQueries({ queryKey: ['todos'] })
// 创建乐观待办事项
const optimisticTodo = { id: uuid(), title: variables.title }
// 将乐观待办事项添加到待办事项列表
queryClient.setQueryData(['todos'], (old) => [...old, optimisticTodo])
// 返回包含乐观待办事项的上下文
return { optimisticTodo }
},
onSuccess: (result, variables, context) => {
// 将待办事项列表中的乐观待办事项替换为结果
queryClient.setQueryData(['todos'], (old) =>
old.map((todo) =>
todo.id === context.optimisticTodo.id ? result : todo,
),
)
},
onError: (error, variables, context) => {
// 从待办事项列表中删除乐观待办事项
queryClient.setQueryData(['todos'], (old) =>
old.filter((todo) => todo.id !== context.optimisticTodo.id),
)
},
retry: 3,
})
// 在某个组件中启动变更:
const mutation = useMutation(() => { mutationKey: ['addTodo'] })
mutation.mutate({ title: '标题' })
// 如果由于设备(例如)离线而暂停了变更,
// 则可以在应用程序退出时对暂停的变更进行脱水:
const state = dehydrate(queryClient)
// 然后可以在应用程序启动时再次对变更进行水合:
hydrate(queryClient, state)
// 恢复暂停的变更:
queryClient.resumePausedMutations()
const queryClient = new QueryClient()
// 定义“addTodo”变更
queryClient.setMutationDefaults(['addTodo'], {
mutationFn: addTodo,
onMutate: async (variables) => {
// 取消待办事项列表的当前查询
await queryClient.cancelQueries({ queryKey: ['todos'] })
// 创建乐观待办事项
const optimisticTodo = { id: uuid(), title: variables.title }
// 将乐观待办事项添加到待办事项列表
queryClient.setQueryData(['todos'], (old) => [...old, optimisticTodo])
// 返回包含乐观待办事项的上下文
return { optimisticTodo }
},
onSuccess: (result, variables, context) => {
// 将待办事项列表中的乐观待办事项替换为结果
queryClient.setQueryData(['todos'], (old) =>
old.map((todo) =>
todo.id === context.optimisticTodo.id ? result : todo,
),
)
},
onError: (error, variables, context) => {
// 从待办事项列表中删除乐观待办事项
queryClient.setQueryData(['todos'], (old) =>
old.filter((todo) => todo.id !== context.optimisticTodo.id),
)
},
retry: 3,
})
// 在某个组件中启动变更:
const mutation = useMutation(() => { mutationKey: ['addTodo'] })
mutation.mutate({ title: '标题' })
// 如果由于设备(例如)离线而暂停了变更,
// 则可以在应用程序退出时对暂停的变更进行脱水:
const state = dehydrate(queryClient)
// 然后可以在应用程序启动时再次对变更进行水合:
hydrate(queryClient, state)
// 恢复暂停的变更:
queryClient.resumePausedMutations()
如果您使用 persistQueryClient 插件持久化离线变更,则除非您提供默认的变更函数,否则在重新加载页面时无法恢复变更。
这是一个技术限制。当持久化到外部存储时,仅持久化变更的状态,因为函数无法序列化。水合后,触发变更的组件可能未挂载,因此调用 resumePausedMutations 可能会产生错误:No mutationFn found。
const persister = createSyncStoragePersister({
storage: window.localStorage,
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 小时
},
},
})
// 我们需要一个默认的变更函数,以便在页面重新加载后可以恢复暂停的变更
queryClient.setMutationDefaults(['todos'], {
mutationFn: ({ id, data }) => {
return api.updateTodo(id, data)
},
})
export default function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister }}
onSuccess={() => {
// 在从 localStorage 成功初始恢复后恢复变更
queryClient.resumePausedMutations()
}}
>
<RestOfTheApp />
</PersistQueryClientProvider>
)
}
const persister = createSyncStoragePersister({
storage: window.localStorage,
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 24 小时
},
},
})
// 我们需要一个默认的变更函数,以便在页面重新加载后可以恢复暂停的变更
queryClient.setMutationDefaults(['todos'], {
mutationFn: ({ id, data }) => {
return api.updateTodo(id, data)
},
})
export default function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister }}
onSuccess={() => {
// 在从 localStorage 成功初始恢复后恢复变更
queryClient.resumePausedMutations()
}}
>
<RestOfTheApp />
</PersistQueryClientProvider>
)
}
我们还有一个广泛的离线示例,涵盖了查询和变更。
默认情况下,所有变更都并行运行——即使您多次调用同一变更的 .mutate()。可以为变更指定一个带有 id 的 scope 以避免这种情况。具有相同 scope.id 的所有变更都将串行运行,这意味着当它们被触发时,如果该作用域中已经有变更正在进行,它们将以 isPaused: true 状态开始。它们将被放入队列中,并在队列中轮到它们时自动恢复。
const mutation = useMutation(() => {
mutationFn: addTodo,
scope: {
id: 'todo',
},
})
const mutation = useMutation(() => {
mutationFn: addTodo,
scope: {
id: 'todo',
},
})
有关变更的更多信息,请参阅社区资源中的 #12:掌握 Solid Query 中的变更。