Apollo 客户端的突变
使用 useMutation 钩子修改数据
既然我们已经学会了如何使用 Apollo Client 从后端查询数据,那么自然下一步就是学习如何Apollo Client来修改后端数据突变。
本篇文章演示了如何使用Apollo 客户端将自己与 GraphQL 服务器的更新发送useMutation
钩子。您还将学习如何在执行突变后更新 Apollo 客户端缓存,以及如何跟踪加载和错误状态。
为了更好地理解以下示例,请打开我们的入门项目和示例 GraphQL 服务器在 CodeSandbox 上。您可以在此查看应用程序的完成版本此处。
先决条件
本文假设您熟悉构建基本 GraphQL mutations。如果您需要复习,我们建议您 阅读此指南。
本文还假设您已经设置了 Apollo Client 并将您的 React 应用程序包裹在 ApolloProvider
组件中。有关这些步骤的帮助,请 开始。
执行突变
React 的钩子 useMutation
是在 Apollo 应用程序中执行 mutations 的主要 API。
要执行一个 mutation,您首先在 React 组件中调用 useMutation
,并传递您要执行
import { gql, useMutation } from '@apollo/client';// Define mutationconst INCREMENT_COUNTER = gql`# Increments a back-end counter and gets its resulting valuemutation IncrementCounter {currentValue}`;function MyComponent() {// Pass mutation to useMutationconst [mutateFunction, { data, loading, error }] = useMutation(INCREMENT_COUNTER);}
如上图所示,您使用 gql
函数将 useMutation
。
当您的组件渲染时,useMutation
返回一个包含以下内容的元组:
- 一个可随时调用以执行 mutate 函数。
- 与
useQuery
不同,useMutation
不会在渲染时自动执行其操作。相反,您需要调用这个修改函数。
- 与
- 一个对象,其中包含代表修改执行当前状态的字段(data、loading等)。
- 此对象与
useQuery
钩子返回的对象类似。具体细节,请参阅结果。
- 此对象与
示例
假设我们正在创建一个待办事项列表应用,并希望用户能够向其列表中添加条目。首先,我们将创建对应的名为ADD_TODO
的GraphQL mutation。请记住将GraphQL字符串用gql
函数包裹,以解析为查询文档:
import { gql, useMutation } from '@apollo/client';const ADD_TODO = gql`mutation AddTodo($type: String!) {addTodo(type: $type) {idtype}}`;
接下来,我们将创建一个名为AddTodo
的组件,它代表待办事项列表的提交表单。在其中,我们将ADD_TODO
mutation传递给useMutation
钩子:
function AddTodo() {let input;const [addTodo, { data, loading, error }] = useMutation(ADD_TODO);if (loading) return 'Submitting...';if (error) return `Submission error! ${error.message}`;return (<div><formonSubmit={e => {e.preventDefault();addTodo({ variables: { type: input.value } });input.value = '';}}><inputref={node => {input = node;}}/><button type="submit">Add Todo</button></form></div>);}
在这个示例中,我们的表单的onSubmit
处理程序调用由useMutation
钩子返回的修改函数(名为addTodo
)。
请注意,此行为与useQuery
不同,它会立即执行其操作。这是因为mutations常在用户操作时执行(例如在这个例子中提交表单)。
提供选项
useMutation
钩子接受一个options
对象作为其第二个参数。以下是一个提供默认值的GraphQL variables
的示例:
const [addTodo, { data, loading, error }] = useMutation(ADD_TODO, {variables: {type: "placeholder",someOtherVariable: 1234,},});
所有受支持的选项列在选项中。
您可以直接向您的修改函数提供选项,如上面示例片段所示:
addTodo({variables: {type: input.value,},});
在这里,我们使用variables
选项提供任何GraphQL 所需的变量的价值(具体来说,type
的创建待办事项项)。
选项优先级
如果同时向useMutation
和您的mutate函数提供相同的选项,则mutate函数的值具有优先级。在variables
选项的具体情况下,两个对象会进行 shallow合并,这意味着只向useMutation
提供的变量将被保留在结果对象中。这可以帮助您为变量设置默认值。
在上面的示例代码段中,input.value
会覆盖"placeholder"
作为type
变量的值。变量someOtherVariable
的值(1234
)将被保留。
跟踪突变状态
除了mutate函数之外,useMutation
钩子返回一个表示突变执行当前状态的对象。该对象中的字段(在结果中列出)包括指示mutate函数是否已被调用
以及突变的结果是否正在加载
中的布尔值。
上面的示例将对象中的loading
和error
字段解构,以根据当前突变的状态对AddTodo
组件进行不同的渲染:
if (loading) return 'Submitting...';if (error) return `Submission error! ${error.message}`;
useMutation
钩子还支持onCompleted
和onError
选项,如果您喜欢使用回调。 查看API参考。
重置突变状态
useMutation
返回的突变结果对象中包含一个reset
函数:
const [login, { data, loading, error, reset }] = useMutation(LOGIN_MUTATION);
调用reset
将重置突变的结果回到其初始状态(即,在调用mutate函数之前)。您可以使用此功能启用用户在UI中忽略突变结果数据或错误。
调用reset
不会删除任何突变执行返回的缓存数据。它只会影响与useMutation
钩子相关的状态,导致相应的组件重新渲染。
function LoginPage () {const [login, { error, reset }] = useMutation(LOGIN_MUTATION);return (<><form><input class="login"/><input class="password"/><button onclick={login}>Login</button></form>{error &&<LoginFailedMessageWindowmessage={error.message}onDismiss={() => reset()}/>}</>);}
更新本地数据
当您执行一个突变时,您会修改后端数据。通常,然后您希望更新您的本地缓存数据以反映后端修改。例如,如果您执行一个将项添加到您的待办事项列表的突变,您还希望该项目出现在您的列表缓存副本中。
支持的方法
更新您的本地数据的直接方法是对可能受突变影响的任何查询进行重新查询。然而,这种方法需要额外的网络请求。
如果你的 mutation 返回了所有修改的对象和字段,你可以 直接更新缓存 而不 需要进行任何后续的网络请求。然而,随着你的 mutations 变得更加复杂,这种方法会变得更加复杂。
如果你刚刚开始使用 Apollo Client,我们建议重新查询以更新缓存的本地数据。完成后,你可以通过直接更新缓存来提高应用程序的响应速度。
重新查询
如果你知道在特定的 mutation 后通常需要重新查询某些查询,你可以在该 mutation
的选项中包含一个 refetchQueries
数组:
// Refetches two queries after mutation completesconst [addTodo, { data, loading, error }] = useMutation(ADD_TODO, {refetchQueries: [GET_POST, // DocumentNode object parsed with gql'GetComments' // Query name],});
你只能重新查询 活动 查询。活动查询是指当前页面上组件使用的查询。如果你想要更新的数据不是由当前页面上的组件获取的,最好是 直接更新缓存。
refetchQueries
数组中的每个元素都是以下之一:
DocumentNode
对象,使用gql
函数处理- 你之前执行的查询的名称,用作字符串(例如,
GetComments
)- 为了按名称引用查询,请确保您的应用中的每个查询都有一个 唯一的 名称。
每个包含的查询都将使用其最近提供的变量集执行。
你可以将 refetchQueries
选项提供给 useMutation
或是突变函数。有关详细信息,请参阅 选项优先级。
请注意,在一个有十几个或上百个不同查询的应用中,确定特定 mutation 后应重新查询哪些查询可能会很具挑战性。
直接更新缓存
包含修改的对象在 mutation 响应中
在大多数情况下,mutation 响应应包含 mutation 修改的任何对象。这使 Apollo Client 能够归一化这些对象并按其 __typename
和 id
字段(默认情况下) 缓存它们。
在上面的例子中,我们的 ADD_TODO
mutation 可能会返回以下结构的 Todo
对象:
{"__typename": "Todo","id": "5","type": "groceries"}
Apollo 客户端 自动将 __typename
字段 添加到查询和突变中的每个对象中,默认情况下。
在接收到此响应对象后, Apollo 客户端 使用键 Todo:5
缓存它。如果存在具有此键的缓存的相同对象, Apollo 客户端 将覆盖任何在突变响应中包括的现有字段(其他现有字段将被保留)。
以这种方式返回修改后的对象是使您的缓存与后端同步的一个有用步骤。但是,这并不总是足够的。例如,新缓存的对象不会被自动添加到现在应包含该对象的任何 列表字段 中。为了实现这一点,您可以定义一个 update
函数。)
update
函数
当一个 突变的响应不足以更新 所有 修改过的字段(比如某些列表字段)在您的缓存中时,您可以定义一个 update
函数来在突变后对缓存的文件进行手动更改。
您将一个 update
函数提供给 useMutation
,如下所示:
const GET_TODOS = gql`query GetTodos {todos {id}}`;function AddTodo() {let input;const [addTodo] = useMutation(ADD_TODO, {update(cache, { data: { addTodo } }) {cache.modify({fields: {todos(existingTodos = []) {const newTodoRef = cache.writeFragment({data: addTodo,fragment: gql`fragment NewTodo on Todo {idtype}`});return [...existingTodos, newTodoRef];}}});}});return (<div><formonSubmit={e => {e.preventDefault();addTodo({ variables: { type: input.value } });input.value = "";}}><inputref={node => {input = node;}}/><button type="submit">Add Todo</button></form></div>);}
如图所示,update
函数接收一个代表 Apollo Client 缓存的 cache
对象。该对象提供了对如 readQuery
/writeQuery
, readFragment
/writeFragment
, modify
和 evict
等缓存 API 方法的访问。这些方法使您能够像与 GraphQL 服务器交互一样在缓存上执行 GraphQL 操作。
关于缓存函数的更多支持信息,请参阅 与缓存数据交互。
此外,update
函数还会接收一个包含 data
属性的对象,该属性包含突变的结果。您可以使用该值使用 cache.writeQuery
, cache.writeFragment
或 cache.modify
更新缓存。
如果您的突变指定了 乐观响应,update
函数会 两次 被调用:一次是获得乐观结果,另一次是在返回突变实际结果时。
在上面的示例中,当 ADD_TODO
突变执行时,新添加并返回的 addTodo
对象在 update
函数运行之前会被自动保存到缓存中。然而,被 GET_TODOS
查询(它监视 ROOT_QUERY.todos
缓存列表)不会自动更新。这意味着 GET_TODOS
查询无法通知新的 Todo
对象,这又意味着查询不会更新以显示新的条目。
为了解决这个问题,我们使用 cache.modify
通过运行 "修改器" 函数,来对缓存进行微创操作 inserting 或 deleting 项。在上面的示例中,我们知道 GET_TODOS
查询的结果存储在缓存中的 ROOT_QUERY.todos
数组中,所以使用 todos
修改器函数来更新缓存的数组,包含对新添加的 Todo
的引用。通过 cache.writeFragment
的帮助,我们获取已添加 Todo
的内部引用,然后将该引用追加到 ROOT_QUERY.todos
数组中。
对update
函数内部缓存的任何更改都会自动广播给监听此数据更改的查询。因此,您的应用程序的UI将更新以反映这些更新的缓存值。
在update
后重新抓取
一个update
函数试图复制mutation's的后端变更到您客户端的本地缓存中。这些缓存修改将广播给所有受影响的活跃查询,从而自动更新您的UI。如果update
函数正确执行,您的用户将立即看到最新数据,而无需等待另一个网络往返。
然而,一个update
函数可能通过设置错误的缓存值而导致复制错误。您可以通过重新抓取受影响的活跃查询来“双重检查”您的update
函数的修改。为此,首先为您的mutate函数提供一个onQueryUpdated
回调函数:
addTodo({variables: { type: input.value },update(cache, result) {// Update the cache as an approximation of server-side mutation effects},onQueryUpdated(observableQuery) {// Define any custom logic for determining whether to refetchif (shouldRefetchQuery(observableQuery)) {return observableQuery.refetch();}},})
您的update
函数完成后,Apollo Client将针对每个有权访问已更新的缓存字段的活跃查询调用一次onQueryUpdated
一次。在onQueryUpdated
内,您可以使用任何自定义逻辑来确定是否要重新抓取相关查询。
要从onQueryUpdated
重新抓取一个查询,如上所示,调用return observableQuery.refetch()
,否则不需要返回任何值。如果重新抓取的查询响应与您的update
函数的修改不同,您的缓存和UI都将自动再次更新。否则,您的用户将看不到任何变化。
有时,可能难以让您的update
函数更新所有相关的查询。并非每个mutation都能提供足够的信息让update
函数有效地执行其工作。为确保某个查询被包含在内,您可以将onQueryUpdated
与refetchQueries: [...]
结合使用:
addTodo({variables: { type: input.value },update(cache, result) {// Update the cache as an approximation of server-side mutation effects.},// Force ReallyImportantQuery to be passed to onQueryUpdated.refetchQueries: ["ReallyImportantQuery"],onQueryUpdated(observableQuery) {// If ReallyImportantQuery is active, it will be passed to onQueryUpdated.// If no query with that name is active, a warning will be logged.},})
如果ReallyImportantQuery
本已因为您的update
函数而被传递给onQueryUpdated
,那么它只会传递一次。使用refetchQueries: ["ReallyImportantQuery"]
仅保证查询将被包含在内。
如果发现已包含比预期的更多查询,可以在检查ObservableQuery
以确定它不需要重新抓取后返回false
以跳过或忽略一个查询,从onQueryUpdated
返回Promise
将导致最终的Promise<FetchResult<TData>>
变异关注点(mutation)等待来自onQueryUpdated
的任何返回的承诺,消除了对遗留awaitRefetchQueries: true
选项的需求。
想要使用 onQueryUpdated
API 而不进行任何 mutation,请尝试使用 client.refetchQueries
方法。在独立 client.refetchQueries
API 中,refetchQueries: [...]
选项被称为 include: [...]
,而 update
函数被称为 updateCache
以提高清晰度。否则,相同的内部系统同时支撑 client.refetchQueries
和 mutation 之后
useMutation
API
以下列出了 useMutation
钩子支持的选择项和结果 字段。
大多数对 useMutation
的调用都可以省略这些选项中的大多数,但了解它们的存在是有用的。有关 useMutation
钩子 API 的详细信息,包括使用示例,请参阅 API参考。
选项
useMutation
钩子接受以下选项:
如果为 true
,则确保在将 mutation 视为已完成之前,完成 refetchQueries
中包含的所有查询。
默认值为 false
(异步地重新获取查询)。
错误策略
指定 mutation 如何处理同时返回 GraphQL 错误和部分结果的响应。
有关详细信息,请参阅 GraphQL 错误策略。
默认值为 none
,这意味着 mutation 的结果包括错误详情,但 不包括 部分结果。
如果为 true
,则不更新 mutation 的数据属性,以包含 mutation 的结果。
默认值为 false
。
(data: TData, clientOptions?: BaseMutationOptions) => void
一个回调函数,在您的 mutation 在没有错误的情况下成功完成(或者如果 errorPolicy
为 ignore
且部分数据返回时)调用。
此函数传入 mutation 的结果 data以及传递给 mutation 的任何选项。
(error: ApolloError, clientOptions?: BaseMutationOptions) => void
当 mutation 遇到错误时调用的回调函数(除非 errorPolicy
设置为 ignore
)。
此函数接收一个包含 ApolloError
对象,无论是包含 networkError
对象还是包含 graphQLErrors
数组的数组,具体取决于发生的错误,以及传递给 mutation 的任何选项。
OnQueryUpdated<any>
捕获缓存由 mutation 更新以及 client.mutate
传递到列表 refetchQueries: [...]
中指定的任何查询的查询的回调函数。
从 onQueryUpdated
返回一个 Promise
将导致最终 mutation 的 Promise
等待返回的 Promise
。返回 false
将忽略查询。
((result: FetchResult<TData>) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude
一个数组(或一个返回数组的函数),指定操作后需要重新获取的查询。
数组的每个值可以是以下之一
一个包含要执行的
query
以及任何variables
的对象表示要重新获取的 操作名称 的字符串
TVariables
一个包含所有 GraphQL 变量,这些变量是 mutation 执行所需的。
对象的每个键对应一个变量名称,该键的值对应变量值。
ApolloClient<object>
使用该实例执行 mutation 的 ApolloClient
实例。
默认情况下,通过上下文传递的实例被使用,但您也可以在这里提供不同的实例。
TContext
如果您使用 Apollo Link,此对象是传递您链接链的 context
对象的初始值。
如果 true
,则在网络状态改变或发生网络错误时,将会重新渲染与正在进行中的突变相关联的组件。
默认值为 false
。
突变缓存策略
如果突变的返回结果不应用缓存在 Apollo 客户端缓存中,可以提供no-cache
。
默认值是 network-only
(意味着结果将被写入缓存)。
与查询不同,突变 不支持 设置除了 fetch policies 以及 no-cache
之外的其他策略。
TData | (vars:TVariables,{ IGNORE }:({ IGNORE:IgnoreModifier;})=> TData | IgnoreModifier)
通过提供一个对象或回调函数,在突变之后被调用,可以让你返回乐观的数据并可选地通过 IGNORE
锁定对象跳过更新,直到突变完成,这可以使得 UI 更新更响应。
更多信息,请参阅 乐观突变结果。
MutationUpdaterFunction<TData,TVariables,TContext,TCache>
这是一个在突变完成后用于更新 Apollo 客户端缓存的功能。
更多信息,请参阅 突变后更新缓存。
为了避免突变根字段 参数保留敏感信息,Apollo 客户端 v3.4+ 在每次突变完成后自动清除缓存中的任何 ROOT_MUTATION
字段。如果你需要这些信息保留在缓存中,可以通过在突变中传递 keepRootFields: true
来防止其被删除。 ROOT_MUTATION
的结果数据也会传递给突变 update
函数,所以我们建议尽可能使用这种方式获取结果,而不是使用此选项。
MutationQueryReducersMap<TData>
A MutationQueryReducersMap
,它是一个从 查询名字到 突变查询还原器的映射。简单来说,此映射定义了如何将突变的成果合并到您应用程序当前监视的查询结果中。
结果
`useMutation`的结果是一个元组,第一个位置是mutate函数,第二个位置是代表突变结果的对象。
您可以通过调用这个mutate函数来从您的UI触发突变。
布尔值
如果为 true
,则表示突变功能的mutate函数已被调用。
ApolloClient<object>
执行突变的服务器端Apollo客户端实例。
这可用于手动执行后续操作或将数据写入缓存。
TData | null
从您的突变返回的数据。如果 ignoreResults
为 true
,则可能为 undefined
。
ApolloError
如果突变产生一个或多个错误,此对象包含一个 graphQLErrors
数组或单个 networkError
。否则,此值为 undefined
。
有关更多信息,请参阅 操作错误处理。
布尔值
如果为 true
,则表示突变当前正在执行中。
() => void
您可以调用此函数来将突变的结果重置为其初始,未被调用的状态。
下一步
`useQuery`和`useMutation
钩子一起代表了Apollo客户端执行 GraphQL 操作的核心API。现在您已经熟悉了这两个,您可以开始利用Apollo客户端的完整功能集,包括:
- 乐观UI:学习如何在服务器返回突变结果之前返回一个乐观响应来提高感知性能。
- 本地状态:使用Apollo客户端通过执行客户端突变来管理您应用程序的全部本地状态。
- Apollo中的缓存:深入Apollo客户端缓存及其规范化的情况。理解缓存对编写您的突变的
update
函数非常有帮助!