本文译自 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 联系我,或者在下方留言。 ⬇️