logo

mutation & 乐观更新 —— react-query 【5】数据变更

Authors
  • avatar
    Name
    White Play
    Twitter

夜晚的鸟巢

夜晚的鸟巢

Unlike queries, mutations are typically used to create/update/delete data or perform server side-effects. For this purpose, TanStack Query exports a useMutation hook.

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

什么是 Mutation

所有状态管理工具本质上都是在做两件事,一个是获取数据,一个是修改数据

比如 react 自己的useState hook,顾名思义,它本质也是一种状态管理。

const [state, setState] = useState(initialState)

它返回的state 是用于获取数据的,setState 是用于修改数据的。

query 中,获取数据对应的是useQuery修改数据对应的是useMutation

我个人理解,就是为POST/PUT/DELETE等请求提供了一个更方便的钩子。

useMutation

const { mutate, isPending, isSuccess, isError } = useMutation({
  mutationFn: () => {
    return axios.post('/api/user', { name: 'jack' })
  },
  // 以下方法在 mutate 各个阶段触发
  onMutate: () => {
    console.log('onMutate')
  },
  onSuccess: () => {
    console.log('onSuccess')
  },
  onError: () => {
    console.log('onError')
  },
  onSettled: () => {
    console.log('onSettled')
  },
})

为什么不直接调用 axios.post(...),而是要通过useMutation来调用?

因为useMutation可以让我们很方便的获取到mutation的衍生状态,比如isPendingisSuccessisError等。而且可以配合retry参数,在请求失败后自动重试。

回调函数中做缓存管理

我们可以通过onSuccessonError等回调函数,在mutation成功或失败后,更方便的做一些额外的操作。

值得注意的是,如果 mutationFn 中返回的 Promise 是reject的,那么就算接口请求成功了,onSuccess 也不会触发。useMutation 的 throwOnError 参数可以控制是否抛出错误,但默认是false换句话说,如果 onSuccess 一直不触发,请将 throwOnError 设为 true,查看是否报错。

const { mutate } = useMutation({
  mutationFn: () => {
    return axios.post('/api/user', { name: 'jack' })
  },
  onSuccess: (newUser) => {
    toast.success('User created successfully')
    // 创建用户成功后,更新缓存中的用户列表
    queryClient.setQueryData(['users', newUser.id], newUser)
    // 或者创建用户成功后,让之前的缓存数据失效,并重新获取数据(即刷新缓存)
    queryClient.invalidateQueries({ queryKey: ['users'] })
  },
})

setQueryData 是用于手动设置缓存数据

invalidateQueries 是用于让之前基于queryKey缓存的数据失效并重新获取数据(即刷新缓存)。

这些api对于前端管理缓存状态非常有用,可以有效提升用户体验。

乐观更新 Optimistic Updates

通常我们提交一个mutation后,需要给用户一个 loading,并等待后端返回结果,确保请求成功后才能更新 UI。

但这样用户体验很不流畅。

我们该如何优化呢?其实,在很多情况下,我们不需要等待后端返回结果就已经可以预知请求成功后的 ui 变化。如果服务器告诉我们请求失败了,大不了提示用户“请求失败稍后重试”,然后回滚 ui。

这就是optimistic updates,也就是乐观更新

以下这些场景非常适合:

  • 点赞
  • 收藏
  • 关注
  • 评论
  • 删除

基础实现

既然是乐观更新,那就不能等后端返回结果再修改状态了。换句话说,我们不应该在onSuccess中修改状态,而是应该在onMutate中修改状态。

怎么修改状态呢?

queryClient提供了setQueryData方法,可以让我们修改缓存的数据。

接下来要思考的问题是,如果网络请求失败了,我们该如何回滚状态?

queryClient提供了getQueryData方法,可以让我们获取缓存的数据。

基于queryKey,我们可以在乐观的修改状态前先保存目前缓存的状态作为快照,再在onMutate方法中返回一个回滚方法。

onError 函数的第三个参数是 context,它是onMutate的返回值,在本例中就是回滚方法。

类似Promise,该 api 还提供了onSettled,它无论接口成功与否,都会执行。我们可以在里面调用invalidateQueries,刷新缓存。

onMutate: async ({ targetData }) => {
  const snapshot = queryClient.getQueryData(queryKey)
  // 乐观更新
  queryClient.setQueryData(queryKey, (oldData) => {
    //handle old data and then
    return newData
  })
  // 回滚
  return () => queryClient.setQueryData(queryKey, snapshot)
}
onError: (err, variables, rollback) => {
  rollback?.()
}

onSettled: () => {
  return queryClient.invalidateQueries({
    queryKey,
  })
}

真实代码

export type Todo = {
  id: number
  title: string
  description: string
  completed: boolean
}

const useCheckTodo = () => {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: ({ todo }: { todo: Todo }) => checkTodo(todo),
    onMutate: ({ todo }) => {
      const snapshot = queryClient.getQueryData(['todoList'])
      queryClient.setQueryData(['todoList'], (oldData: Todo[]) => {
        return oldData.map((t) => (t.id === todo.id ? { ...t, completed: !t.completed } : t))
      })
      /**
       * The value returned from this function will be passed to both the onError and onSettled functions in the event of a mutation failure and can be useful for rolling back optimistic updates.
       */
      return () => {
        queryClient.setQueryData(['todoList'], snapshot)
      }
    },
    onError: (error, todo, rollback) => {
      rollback?.()
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['todoList'] })
    },
  })
}

hook 封装

如果常用乐观更新这种操作,我们可以将它封装成一个 hook,方便复用。

但我觉得一个项目中涉及到乐观更新的地方其实并不多,基于useMutate调用onMutateonErroronSettled等方法,可以很方便的实现乐观更新。封装了 Hook 后,反而增加了代码的复杂度。

export const useOptimisticMutation = ({ mutationFn, queryKey, updater, invalidates }) => {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn,
    onMutate: async () => {
      // 取消任何相关请求,避免覆盖结果
      await queryClient.cancelQueries({
        queryKey,
      })

      const snapshot = queryClient.getQueryData(queryKey)

      queryClient.setQueryData(queryKey, updater)

      return () => {
        queryClient.setQueryData(queryKey, snapshot)
      }
    },
    onError: (err, variables, rollback) => {
      rollback?.()
    },
    onSettled: () => {
      return queryClient.invalidateQueries({
        queryKey: invalidates,
      })
    },
  })
}