修改 Mutations

与查询(queries)不同,变更通常用于创建/更新/删除数据或执行服务器端副作用。为此,TanStack Query 导出了一个 useMutation Hook。

以下是一个将新待办事项添加到服务器的变更示例:

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

在任何给定时刻,一个变更只能处于以下状态之一:

  • isIdlestatus === 'idle' - 变更当前处于空闲或初始/重置状态

  • isPendingstatus === 'pending' - 变更正在运行

  • isErrorstatus === 'error' - 变更遇到了错误

  • isSuccessstatus === 'success' - 变更成功,且变更数据可用

除了这些主要状态外,根据变更的状态,还可以获得更多信息:

  • error - 如果变更处于 error 状态,可以通过 error 属性获取错误信息。

  • data - 如果变更处于 success 状态,可以通过 data 属性获取数据。

在上面的示例中,你还看到可以通过向 mutate 函数传递单个变量或对象来为你的变更函数提供变量。

即使只有变量,变更也并非特别之处,但当与 onSuccess 选项、Query Client 的 invalidateQueries 方法 和 Query Client 的 setQueryData 方法 一起使用时,变更就成为了一个非常强大的工具。

重要mutate 函数是一个异步函数,这意味着你不能在 React 16 及更早版本的事件回调中直接使用它。如果你需要在 onSubmit 中访问事件,你需要将 mutate 包装在另一个函数中。这是由于 React 事件池化 造成的。

// 在 React 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>
}

重置变更状态

有时你可能需要清除变更请求的 errordata。为此,你可以使用 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>
  )
}

变更副作用

useMutation 提供了一些辅助选项,允许在变更生命周期的任何阶段快速轻松地执行副作用。这些选项对于在变更后使查询失效并重新获取以及乐观更新都非常有用。

useMutation({
  mutationFn: addTodo,
  onMutate: (variables, context) => {
    // 变更即将发生!

    // 可选择返回一个包含数据的结果,用于例如回滚操作
    return { id: 1 }
  },
  onError: (error, variables, onMutateResult, context) => {
    // 发生错误了!
    console.log(`使用 id ${onMutateResult.id} 回滚乐观更新`)
  },
  onSuccess: (data, variables, onMutateResult, context) => {
    // 成功了!
  },
  onSettled: (data, error, variables, onMutateResult, context) => {
    // 成功或失败……都无所谓!
  },
})

如果在任何回调函数中返回一个 Promise,它将首先被等待,然后才会调用下一个回调:

useMutation({
  mutationFn: addTodo,
  onSuccess: async () => {
    console.log("我是第一个!")
  },
  onSettled: async () => {
    console.log("我是第二个!")
  },
})

你可能会发现,在调用 mutate 时,你想触发除了 useMutation 中定义的之外的额外回调。这可以用来触发组件特定的副作用。为此,你可以在 mutate 函数的变量之后提供任何相同的回调选项。支持的选项包括:onSuccessonErroronSettled。请注意,如果在变更完成前组件已经卸载,这些额外的回调将不会运行。

useMutation({
  mutationFn: addTodo,
  onSuccess: (data, variables, onMutateResult, context) => {
    // 我将先触发
  },
  onError: (error, variables, onMutateResult, context) => {
    // 我将先触发
  },
  onSettled: (data, error, variables, onMutateResult, context) => {
    // 我将先触发
  },
})

mutate(todo, {
  onSuccess: (data, variables, onMutateResult, context) => {
    // 我将第二个触发!
  },
  onError: (error, variables, onMutateResult, context) => {
    // 我将第二个触发!
  },
  onSettled: (data, error, variables, onMutateResult, context) => {
    // 我将第二个触发!
  },
})

连续变更

在处理连续变更时,处理 onSuccessonErroronSettled 回调的方式略有不同。当这些回调作为参数传递给 mutate 函数时,它们将只执行一次,并且只有在组件仍然挂载时才会执行。这是因为每次调用 mutate 函数时,变更观察器都会被移除并重新订阅。相反,useMutation 的处理程序会为每次 mutate 调用执行。

请注意,传递给 useMutationmutationFn 很可能是异步的。在这种情况下,变更完成的顺序可能与 mutate 函数调用的顺序不同。

useMutation({
  mutationFn: addTodo,
  onSuccess: (data, variables, onMutateResult, context) => {
    // 将被调用 3 次
  },
})

const todos = ['待办事项 1', '待办事项 2', '待办事项 3']
todos.forEach((todo) => {
  mutate(todo, {
    onSuccess: (data, variables, onMutateResult, context) => {
      // 将只执行一次,针对最后一次变更(待办事项 3),
      // 无论哪个变更先完成
    },
  })
})

Promise

使用 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('完成')
}

重试

默认情况下,TanStack Query 在出错时不会重试变更,但可以通过 retry 选项实现:

const mutation = useMutation({
  mutationFn: addTodo,
  retry: 3,
})

如果变更因设备离线而失败,当设备重新连接时,它们将按相同顺序重试。

持久化变更

如果需要,变更可以持久化到存储中,并在稍后恢复。这可以通过 hydration 函数来实现:

const queryClient = new QueryClient()

// 定义 "addTodo" 变更
queryClient.setMutationDefaults(['addTodo'], {
  mutationFn: addTodo,
  onMutate: async (variables, context) => {
    // 取消对 todos 列表的当前查询
    await context.client.cancelQueries({ queryKey: ['todos'] })

    // 创建乐观的 todo
    const optimisticTodo = { id: uuid(), title: variables.title }

    // 将乐观的 todo 添加到 todos 列表
    context.client.setQueryData(['todos'], (old) => [...old, optimisticTodo])

    // 返回一个包含乐观 todo 的结果
    return { optimisticTodo }
  },
  onSuccess: (result, variables, onMutateResult, context) => {
    // 用结果替换 todos 列表中的乐观 todo
    context.client.setQueryData(['todos'], (old) =>
      old.map((todo) =>
        todo.id === onMutateResult.optimisticTodo.id ? result : todo,
      ),
    )
  },
  onError: (error, variables, onMutateResult, context) => {
    // 从 todos 列表中移除乐观的 todo
    context.client.setQueryData(['todos'], (old) =>
      old.filter((todo) => todo.id !== onMutateResult.optimisticTodo.id),
    )
  },
  retry: 3,
})

// 在某个组件中启动变更:
const mutation = useMutation({ mutationKey: ['addTodo'] })
mutation.mutate({ title: '标题' })

// 如果变更因设备离线而暂停,
// 那么当应用程序退出时,可以对暂停的变更进行脱水(dehydration):
const state = dehydrate(queryClient)

// 当应用程序启动时,可以再次对变更进行补水(hydration):
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>
  )
}

我们还有一个全面的 离线示例,涵盖了查询和变更。

变更作用域

默认情况下,所有变更都是并行运行的——即使你多次调用同一个变更的 .mutate()。可以为变更指定一个带有 idscope 来避免这种情况。所有具有相同 scope.id 的变更将串行运行,这意味着当它们被触发时,如果该作用域中已有变更正在进行,它们将从 isPaused: true 状态开始。它们将被放入队列,并在轮到它们时自动恢复。

const mutation = useMutation({
  mutationFn: addTodo,
  scope: {
    id: 'todo',
  },
})

进一步阅读

有关变更的更多信息,请查看社区资源中的 #12: 掌握 React Query 中的变更。

最后更新于

这有帮助吗?