Better Server State Management through TanStack Query!

如何通过 TanStack Query 构建更好的前端数据获取体验

前言

先前文章提到:如何优雅的于 Vue 正确的处理资料载入点出了一些获取外部资料延伸带来的问题与实际解方,这次我想通过 TanStack Query🔗 的视角挖掘怎么构建更好的前端资料获取体验。

前端面对「服务器状态」

  • 远端持久化到无法控制或拥有的位置
  • 需要异步 API 来获取和更新数据
  • 共享所有权,可能会在不知情的情况下改动,不小心会「过期」

前端需要频繁的与「服务器状态 Server State🔗」打交道,过程中的状态获取、管理、同步与缓存都是让人头痛的问题。实际工作规模通常会遇到「满天飞的状态」与「重复逻辑」,八成都与服务器状态脱不了关系。

必须说我的前端生涯花费太多时间处理与理解琐碎的服务器状态了,期望使用恰当的 TanStack Query 抽象,可以极大改善异步状态开发体验,甚至用户体验!

TanStack Query 解决什么问题?

像是进阶一点的元框架如:Next🔗Nuxt🔗 会有既定的方式与整合好的数据获取方案希望你采用。而 TanStack Query🔗 是一款轻巧支持所有热门前端框架的异步状态管理方案,从以下简单的范例与三个概念开始:useQueryqueryKeyqueryFn

<script setup>
import { useQuery } from '@tanstack/vue-query'
const { isPending, isFetching, isError, data, error } = useQuery({
queryKey: ['todos'],
queryFn: getTodos,
})
</script>
  • useQuery🔗 为(Composable / Hook)管理并返回状态
  • queryKey🔗 为独一无二的辨别用字符串数组用于判断缓存与重载入
  • queryFn🔗 为返回 Promise 的函数
const fetchTodos = (): Promise<Todo[]> =>
axios.get('/todos').then((response) => response.data)

自动缓存与数据同步

每一次的 API 请求都会被自动缓存,并根据 queryKey 自动识别与合并。当在不同组件中使用相同的 queryKey 查询只会执行一次网络请求并共享结果。这也意味着切换页面时无需重新请求,节省资源与提升用户体验。

背景更新

Stale-While-Revalidate(SWR):「先用旧的资料马上回应使用者,同时在背景中更新新的资料,下一次再用新的资料。」

资料可能随时在服务器端被其他用户或行为更新,TanStack Query 支持自动或手动触发「背景更新」,让 UI 保持最新资讯但不打断用户操作流程。例如滑动列表时不会闪烁,但背景已经默默更新完毕。

过期逻辑

const { isPending, isFetching, isError, data, error } = useQuery({
queryKey: ['todos'],
queryFn: getTodos,
staleTime: 60 * 1000, // 1 minute
})

可以精细控制数据何时被视为「过期」,通过 staleTime 参数告诉系统在多久之内数据算是有效的,避免每次渲染都触发 API。帮助在「数据即时性」与「性能」之间找到平衡。

并行 Fetching

原生 JS 除非使用 Promise.all🔗 否则 async await 使用上实际是串行执行,而 TanStack Query 默认最大化并行执行,且提供更方便优雅的方式管理请求状态:

// Demo-1
await fetchUser();
await fetchPosts();
// Demo-2
await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
// TanStack Query
const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers })
const teamsQuery = useQuery({ queryKey: ['teams'], queryFn: fetchTeams })
const projectsQuery = useQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
})

依赖 Fetching

一个 Query 依赖另外一个 Query 执行也是常有的事,举例来说需要取得某用户再取得该用户的资源:

// Get the user
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: () => getUserByEmail(email.value),
})
const userId = computed(() => user.value?.id)
const enabled = computed(() => !!user.value?.id)
// Then get the user's projects
const { isIdle, data: projects } = useQuery({
queryKey: ['projects', userId],
queryFn: () => getProjectsByUser(userId.value),
enabled, // The query will not execute until `enabled == true`
})

执行具副作用行为使用 useMutation

const { mutate, isPending } = useMutation({
mutationFn: async (newTodo) => {
const { data } = await axios.post("/todos", newTodo)
return data
},
onSucess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"]})
}
})

相较 useQuery 声明式的读取数据,useMutation 命令式的处理副作用(创建、更新、删除)且可以手动调用 invalidateQueries 来让相关缓存失效并自动重新拉数据。这样不需要手动维护数据同步的逻辑。

总结

基本上 useQueryuseMutationinvalidateQueries 足以应付管理大多数 CRUD 应用,一些进阶的功能如乐观更新🔗分页🔗都可以到官方文件🔗了解更多。

延伸阅读