React Query Effective Query Keys

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

在向你展示我个人如何组织Query Keys以更有效地执行这些操作之前,让我们快速了解一下这三个要点的含义。

缓存数据

在内部,查询缓存只是一个JavaScript对象,其中键是序列化的Query Keys,值是你的查询数据加上元信息。键以deterministic way进行散列,因此你也可以使用对象(但在顶层,键必须是字符串或数组)。

最重要的部分是,键对于你的查询必须是唯一的。如果React Query在缓存中找到一个键的条目,它就会用它。还请注意,你不能将相同的键用于 useQueryuseInfiniteQuery。毕竟,只有一个查询缓存,数据会在这两者之间共享。这会造成问题,因为无限查询与常规查询具有根本不同的结构。

 1useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
 2
 3// 🚨 这不可行
 4useInfiniteQuery({ queryKey: ['todos'], queryFn: fetchInfiniteTodos })
 5
 6// ✅ 选择其他内容
 7useInfiniteQuery({
 8  queryKey: ['infiniteTodos'],
 9  queryFn: fetchInfiniteTodos,
10})

自动重新获取

查询是声明性的。

这是一个非常重要的概念,怎么强调都不过分,而且这也是可能需要一些时间来理解的概念。大多数人以命令式的方式思考查询,尤其是重新获取。

我有一个查询,它获取一些数据。现在我点击这个按钮,我想要重新获取,但使用不同的参数。我看到过许多类似下面这样的尝试:

1function Component() {
2  const { data, refetch } = useQuery({
3    queryKey: ['todos'],
4    queryFn: fetchTodos,
5  })
6
7  // ❓ 如何将参数传递给 refetch ❓
8  return <Filters onApply={() => refetch(???)} />
9}

答案是:你不需要这样做。

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

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

 1function Component() {
 2  const [filters, setFilters] = React.useState()
 3  const { data } = useQuery({
 4    queryKey: ['todos', filters],
 5    queryFn: () => fetchTodos(filters),
 6  })
 7
 8  // ✅ 设置本地状态,让其*驱动*查询
 9  return <Filters onApply={setFilters} />
10}

通过setFilters更新引起的重新渲染将向React Query传递一个不同的Query Keys,这将让它去重新获取数据。在#1: Practical React Query - Treat the query key like a dependency array中有一个更详细的示例。

手动交互

与查询缓存的手动交互是你的Query Keys结构最重要的部分。许多交互方法,如invalidateQueriessetQueriesData,支持Query Filters,允许你模糊匹配Query Keys。

有效的React Query Keys

请注意,这些要点反映了我的个人观点(实际上,这个博客上的所有内容都是如此),所以不要将其视为在使用Query Keys时必须遵循的规定。我发现这些策略在应用程序变得更复杂时效果最好,并且它们的扩展性也非常好。在制作一个待办事项应用程序时,你绝对不需要这样做 😁。

放在一起

如果你还没有阅读过Kent C. Dodds的文章Maintainability through colocation,请务必读一下。我不认为将所有Query Keys全局存储在 /src/utils/queryKeys.ts 中有什么用。我会把我的Query Keys与相应的Queries放在同一个功能目录下,例如:

1- src
2  - features
3    - Profile
4      - index.tsx
5      - queries.ts
6    - Todos
7      - index.tsx
8      - queries.ts

queries文件包含了与React Query相关的所有内容。我通常只导出自定义hooks,因此实际的查询函数和Query Keys将保持局部。

总是使用数组键

是的,Query Keys也可以是字符串,但为了保持统一,我倾向于数组。React Query会在内部将其转换为数组,所以:

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

更新:在 React Query v4 中,所有Key都需要是数组。

结构

把Query Keys从最通用最特定进行结构化,之间的粒度层级取决于你认为合适的数量。下面是一个我如何为待办事项列表结构化Query Keys的示例,该列表允许使用筛选器和详细视图:

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

通过这种结构,我可以使用['todos']来无效化与待办事项相关的所有内容,无论是列表还是详细视图,也可以针对特定的列表进行定位,如果我知道确切的键。更新Mutation的返回值会让操作变得更加灵活,因为你可以在需要时对所有列表进行操作:

 1function useUpdateTitle() {
 2  return useMutation({
 3    mutationFn: updateTitle,
 4    onSuccess: (newTodo) => {
 5      // ✅ 更新待办事项的详细信息
 6      queryClient.setQueryData(['todos', 'detail', newTodo.id], newTodo)
 7
 8      // ✅ 更新包含此待办事项的所有列表
 9      queryClient.setQueriesData(['todos', 'list'], (previous) =>
10        previous.map((todo) => (todo.id === newTodo.id ? newtodo : todo))
11      )
12    },
13  })
14}

但是如果列表和详细视图的结构差异很大,则可能不起作用,不过你还可以无效化所有列表:

 1function useUpdateTitle() {
 2  return useMutation({
 3    mutationFn: updateTitle,
 4    onSuccess: (newTodo) => {
 5      queryClient.setQueryData(['todos', 'detail', newTodo.id], newTodo)
 6
 7      // ✅ 只无效化所有列表
 8      queryClient.invalidateQueries({ queryKey: ['todos', 'list'] })
 9    },
10  })
11}

如果你知道当前所在的列表,例如通过从URL读取筛选器,就可以构造出精确的Query Keys,你还可以将这两种方法结合起来,在列表上调用setQueryData并无效化其他所有列表:

 1function useUpdateTitle() {
 2  // 假设有一个自定义钩子,返回当前筛选器,
 3  // 存储在 URL 中
 4  const { filters } = useFilterParams()
 5
 6  return useMutation({
 7    mutationFn: updateTitle,
 8    onSuccess: (newTodo) => {
 9      queryClient.setQueryData(['todos', 'detail', newTodo.id], newTodo)
10
11      // ✅ 即时更新当前列表
12      queryClient.setQueryData(['todos', 'list', { filters }], (previous) =>
13        previous.map((todo) => (todo.id === newTodo.id ? newTodo : todo))
14      )
15
16      // ✅ 无效化所有其他列表
17      queryClient.invalidateQueries({ queryKey: ['todos', 'list'], exact: false })
18    },
19  })
20}

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

使用Query Key工厂

在上面的示例中,你可以看到我手动声明了很多Query Keys。这不仅容易出错,而且在将来进行更改时变得更加困难,例如,如果你想要为键添加另一个级别。

这就是为什么我建议每个功能使用一个Query Key工厂。它只是一个简单的对象,包含条目和生成Query Key的函数,你可以在自定义hooks中使用这些键。对于上面的示例结构,它可能如下所示:

1const todoKeys = {
2  all: ['todos'] as const,
3  lists: () => [...todoKeys.all, 'list'] as const,
4  list: (filters: string) => [...todoKeys.lists(), { filters }] as const,
5  details: () => [...todoKeys.all, 'detail'] as const,
6  detail: (id: number) => [...todoKeys.details(), id] as const,
7}

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

 1// 🕺 移除与待办事项相关的所有内容
 2queryClient.removeQueries({ queryKey: todoKeys.all })
 3
 4// 🚀 使所有列表无效
 5queryClient.invalidateQueries({ queryKey: todoKeys.lists() })
 6
 7// 🙌 预取单个待办事项
 8queryClient.prefetchQueries({
 9  queryKey: todoKeys.detail(id),
10  queryFn: () => fetchTodo(id),
11})

今天就到这里。如果你有任何问题,请随时在 Twitter 上与我联系,或者在下面留言。

comments powered by Disqus