Effective React Query Keys

695 阅读6分钟

本文译自 TkDodo 的 Effective React Query Keys

Query Keys 是 React Query 中非常重要的核心概念。它们是必要的,这样库在内部才能正确地缓存您的数据,并在查询依赖项发生更改时自动重新获取数据。最后,它还允许您在需要时手动与查询缓存进行交互,例如在执行变更后更新数据或手动使某些查询失效。

让我们在向您展示我个人如何组织 Query Keys 以更有效地完成这些任务之前,快速了解一下这三个要点的含义。

缓存数据

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

最重要的部分是,键必须对于您的查询是唯一的。如果 React Query 在缓存中找到一个键的条目,它将使用它。请注意,您不能将相同的键同时用于 useQuery useInfiniteQuery。毕竟只有一个查询缓存,这样您会在这两者之间共享数据。这是不好的,因为无限查询与“常规”查询在结构上有根本的区别。

不要混合使用键

useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

// 🚨 这样不起作用
useInfiniteQuery({ queryKey: ['todos'], queryFn: fetchInfiniteTodos })

// ✅ 请使用其他键名
useInfiniteQuery({
  queryKey: ['infiniteTodos'],
  queryFn: fetchInfiniteTodos,
})

自动重新获取

查询是声明式的。

这是一个非常重要的概念,无法强调其重要性,这也可能需要一些时间才能理解。大多数人以一种命令式的方式考虑查询,尤其是重新获取数据。

我有一个查询,它获取一些数据。现在我点击了这个按钮,我想要重新获取,但使用不同的参数。我见过许多尝试看起来像这样的代码:

命令式重新获取

function Component() {
  const { data, refetch } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  // ❓ 如何将参数传递给 refetch ❓
  return <Filters onApply={() => refetch(???)} />
}

答案是:不需要传递参数

因为这不是 refetch 的目的 - 它的目的是使用相同的参数重新获取数据。

如果您有一些会更改数据的状态,您只需要将它放在 Query Key 中,因为 React Query 会在键发生更改时自动触发重新获取。因此,当您想要应用筛选器时,只需更改客户端状态

查询键驱动查询

function Component() {
  const [filters, setFilters] = React.useState()
  const { data } = useQuery({
    queryKey: ['todos', filters],
    queryFn: () => fetchTodos(filters),
  })

  // ✅ 设置本地状态,并让它“驱动”查询
  return <Filters onApply={setFilters} />
}

setFilters 更新引发的重新渲染将向 React Query 传递一个不同的 Query Key,从而使其重新获取数据。在 #1: Practical React Query - Treat the query key like a dependency array 中,我有一个更详细的示例。

手动交互

手动与查询缓存进行交互时,您的 Query Key 结构变得非常重要。许多交互方法(例如 invalidateQueries 或 setQueriesData)支持 Query Filters,它们允许您模糊匹配查询键。

高效的 React Query Keys

请注意,这些要点反映了我的个人观点(实际上,这个博客中的所有内容都是如此),因此在处理 Query Key 时,请不要将其视为您必须绝对遵循的内容。我发现这些策略在您的应用程序变得更加复杂时效果最好,而且它们也很容易扩展。当然,对于一个 Todo 应用程序,您完全不需要这么做 😁。

放在一起

如果您还没有阅读过 Kent C. Dodds 撰写的《通过合理的放置提高代码可维护性》,请务必阅读一下。我认为将所有 Query Keys 全局存储在 /src/utils/queryKeys.ts 中并不能使事情变得更好。我将我的 Query Keys 放在它们相应的查询旁边,共同放置在功能目录中,就像这样:

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

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

始终使用数组作为键

是的,查询键也可以是字符串,但为了保持统一,我喜欢始终使用数组。React Query 会在内部将它们转换为数组,所以:

永远使用 Keys 数组

// 🚨 无论如何都会转换为 ['todos']
useQuery({ queryKey: 'todos' })
// ✅
useQuery({ queryKey: ['todos'] })

更新:使用 React Query v4,所有键都需要是数组。

结构

将您的查询键从最通用最具体进行结构化,中间可以有任意级别的细化。以下是我如何为 todos 列表进行结构化,以便支持可过滤的列表和详细视图:

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

通过这种结构,我可以使用 ['todos'] 来使所有与待办事项相关的查询失效,也可以使所有列表或所有详细信息失效,还可以根据确切的键来定位特定的列表。使用 Mutation Responses 进行更新会变得更加灵活,因为如果需要,您可以对所有列表进行操作:

使用 Mutation Responses 进行更新

function useUpdateTitle() {
  return useMutation({
    mutationFn: updateTitle,
    onSuccess: (newTodo) => {
      // ✅ 更新待办事项的详细信息
      queryClient.setQueryData(['todos', 'detail', newTodo.id], newTodo)

      // ✅ 更新包含此待办事项的所有列表
      queryClient.setQueriesData(['todos', 'list'], (previous) =>
        previous.map((todo) => (todo.id === newTodo.id ? newtodo : todo))
      )
    },
  })
}

如果列表和详细信息的结构差异很大,这种方式可能行不通,因此您也可以只使所有列表失效:

使所有列表失效

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

      // ✅ 只是使所有列表失效
      queryClient.invalidateQueries({ queryKey: ['todos', 'list'] })
    },
  })
}

如果您知道当前正在哪个列表上,例如通过从 URL 中读取过滤器,因此可以构建出确切的查询键,您还可以将这两种方法结合起来,在列表上调用 setQueryData,并使其他所有列表失效:

合并

function useUpdateTitle() {
  // 假设有一个返回当前过滤器的自定义钩子,
  // 存储在 URL 中
  const { filters } = useFilterParams()

  return useMutation({
    mutationFn: 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,
      })
    },
  })
}

更新:在 v4 中,refetchActive 已被 refetchType 替换。在上面的例子中,应为 refetchType: 'none',因为我们不想重新获取任何数据。

使用查询键工厂

在上面的例子中,您可以看到我手动声明了很多查询键。这不仅容易出错,而且使未来的更改更加困难,例如,如果您希望为键添加更多级别。

因此,我建议为每个功能使用一个查询键工厂。它只是一个简单的对象,包含条目和产生查询键的函数,您可以在自定义钩子中使用它们。对于上面的例子结构,它可能如下所示:

查询键工厂

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({ queryKey: todoKeys.all })

// 🚀 使所有列表失效
queryClient.invalidateQueries({ queryKey: todoKeys.lists() })

// 🙌 预取单个待办事项
queryClient.prefetchQueries({
  queryKey: todoKeys.detail(id),
  queryFn: () => fetchTodo(id),
})

今天就介绍到这里。如果您有任何问题,随时通过 twitter 联系我,或者在下方留言。 ⬇️