[翻译] Part 8. 在 React-Query 中高效的使用查询键

3,031 阅读6分钟

原文 tkdodo.eu/blog/effect…

Query KeysReact Query 中一个非常重要的核心概念。它们是必要的,以便库可以在内部正确缓存你的数据,并在对查询的依赖项发生变化时自动重新获取。最后,它允许你在需要时手动与缓存交互,例如在 mutation 后更新数据或需要手动使某些查询无效时。

在向你展示我个人如何组织查询键以最有效地完成这些事情之前,让我们快速了解一下这三点的含义。

缓存数据

在内部,缓存只是一个 JavaScript 对象,其中键是序列化的查询键,值是你的查询数据和元信息。键以确定性的方式对其进行哈希处理,因此你也可以使用对象(但在顶层,键必须是字符串或数组)。

最重要的部分是键对于你的查询必须是唯一的。如果 React Query 在缓存中找到键的条目,它将使用缓存。另请注意,你不能对 useQueryuseInfiniteQuery 使用相同的键。毕竟,只有一个 Query Cache,你将在这两者之间共享数据,这并不好,因为无限查询与“普通”查询有着根本不同的结构。

useQuery(['todos'], fetchTodos)

// 🚨 不起作用
useInfiniteQuery(['todos'], fetchInfiniteTodos)

// ✅ 使用别的 key 名
useInfiniteQuery(['infiniteTodos'], fetchInfiniteTodos)

自动重取

查询是声明性的。

这是一个非常重要的概念,怎么强调都不为过,也是可能需要一些时间才能明悟的东西。大多数人以命令式的方式考虑查询,尤其是重新获取。

我有一个查询,它获取一些数据。现在我点击这个按钮,我想重新获取,但使用不同的参数。我见过很多这样的尝试:

// 命令式重取
function Component() {
  const { data, refetch } = useQuery(['todos'], fetchTodos)

  // ❓ 要怎么传递新的参数进行重取 ❓
  return <Filters onApply={() => refetch(???)} />
}

答案是:你没有

这不是 refetch 的用途 - 它用于使用相同的参数重新获取。

如果你有一些改变你数据的状态,你需要做的就是把它放在 Query Key 中,因为每当 key 发生变化时,React Query 都会自动触发重新获取。因此,当你想应用过滤器时,只需更改你的客户端状态:

// 查询键驱动查询
function Component() {
  const [filters, setFilters] = React.useState()
  const { data } = useQuery(['todos', filters], () => fetchTodos(filters))

  // ✅ set local state and let it "drive" the query
  return <Filters onApply={setFilters} />
}

setFilters 更新触发的重新渲染会将不同的 Query Key 传递给 React Query,这将使其重新获取。我在 第一部分—— react-query 实践中 - 将查询键视为依赖数组 中有一个更深入的示例。

手动交互

与查询缓存的手动交互是查询键结构最重要的地方。许多交互方法,如 invalidateQueriessetQueriesData 支持查询 过滤器 ,它允许你模糊地匹配你的查询键。

有效的 React 查询键

请注意,这些观点反映了我的个人观点(实际上,就像本博客上的所有内容一样),因此不要将其视为在使用查询键时绝对必须做的事情。我发现当你的应用程序变得更加复杂时,这些策略最有效,并且它的扩展性也很好。

并置

如果你还没有阅读 Kent C. Dodds 撰写的通过托管的可维护性,请阅读。我不相信将所有查询键全局存储在 /src/utils/queryKeys.ts 中会让事情变得更好。我将我的查询键放在它们各自的查询旁边,并位于功能目录中,例如:

- src
  - features
    - Profile
      - index.tsx
      - queries.ts
    - Todos
      - index.tsx
      - queries.ts

查询文件将包含与 React Query 相关的所有内容。我通常只导出自定义钩子,因此实际的查询函数和查询键将保留在本地。

始终使用数组键

是的,查询键也可以是字符串,但为了保持统一,我喜欢总是使用数组。无论如何,React Query 都会在内部将它们转换为 Array,因此:

// 始终使用数组键
// 🚨 内部会转成 ['todos']
useQuery('todos')
// ✅
useQuery(['todos'])

结构

将你的查询键从最通用到最具体的结构化,使用你认为合适的粒度级别。下面是我将如何构建一个允许可过滤列表和详细视图的待办事项列表:

['todos', 'list', { filters: 'all' }]
['todos', 'list', { filters: 'done' }]
['todos', 'detail', 1]
['todos', 'detail', 2]

使用这种结构,我可以使与 ['todos'] 相关的所有待办事项、所有列表或所有详细信息无效,并且如果我知道确切的键,则可以定位一个特定列表。来自 Mutation Responses 的更新因此变得更加灵活,因为你可以在必要时定位所有列表:

//  来自mutation响应的更新
function useUpdateTitle() {
  return useMutation(updateTitle, {
    onSuccess: (newTodo) => {
      // ✅ 更新 todo 详情
      queryClient.setQueryData(['todos', 'detail', newTodo.id], newTodo)

      // ✅ 更新所有包含这个 todo 的 list
      queryClient.setQueriesData(['todos', 'list'], (previous) =>
        previous.map((todo) => (todo.id === newTodo.id ? newtodo : todo))
      )
    },
  })
}

如果列表的结构和详细信息差异很大,这可能不起作用,因此,你当然也可以使所有列表无效:

// 使所有列表无效
function useUpdateTitle() {
  return useMutation(updateTitle, {
    onSuccess: (newTodo) => {
      queryClient.setQueryData(['todos', 'detail', newTodo.id], newTodo)

      // ✅ 使所有列表无效
      queryClient.invalidateQueries(['todos', 'list'])
    },
  })
}

如果你知道当前在哪个列表中,例如通过从 url 读取过滤器,因此可以构造确切的查询键,你还可以组合这两种方法并在你的列表中调用 setQueryData 并使所有其他方法无效:

// 结合
function useUpdateTitle() {
  // imagine a custom hook that returns the current filters,
  // stored in the url
  const { filters } = useFilterParams()

  return useMutation(updateTitle, {
    onSuccess: (newTodo) => {
      queryClient.setQueryData(['todos', 'detail', newTodo.id], newTodo)

      // ✅ 更新当前的列表
      queryClient.setQueryData(['todos', 'list', { filters }], (previous) =>
        previous.map((todo) => (todo.id === newTodo.id ? newtodo : todo))
      )

      // 🥳 使列表失效,但是不重新请求活跃的
      queryClient.invalidateQueries({
        queryKey: ['todos', 'list'],
        refetchActive: false,
      })
    },
  })
}

使用查询键工厂

在上面的示例中,你可以看到我手动声明了很多查询键。这不仅容易出错,而且使将来更难更改,例如,如果你发现要为密钥添加另一个级别的粒度。

这就是我推荐每个功能一个查询密钥工厂的原因。它只是一个带有条目和函数的简单对象,可以生成查询键,然后你可以在自定义钩子中使用这些键。对于上面的示例结构,它看起来像这样:

// 查询键工厂
const todoKeys = {
  all: ['todos'] as const,
  lists: () => [...todoKeys.all, 'list'] as const,
  list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
  details: () => [...todoKeys.all, 'detail'] as const,
  detail: (id: number) => [...todoKeys.details(), id] as const,
}

这给了我很大的灵活性,因为每个级别都建立在彼此之上,但仍然可以独立访问:

// 🕺 删除全部相关数据
queryClient.removeQueries(todoKeys.all)

// 🚀 使列表无效
queryClient.invalidateQueries(todoKeys.lists())

// 🙌 预取一个 todo 的详情
queryClient.prefetchQueries(todoKeys.detail(id), () => fetchTodo(id))