导航
导航
文章目录󰁋
  1. 一、为什么React应用需要React Query
    1. 1.1 服务端状态的特殊性
    2. 1.2 传统方案的时代局限
  2. 二、React Query核心概念解析
    1. 2.1 QueryKey查询键的重要性
    2. 2.2 查询状态与获取状态
  3. 三、快速上手与基础用法
    1. 3.1 环境安装配置
    2. 3.2 第一个查询用例
    3. 3.3 重要默认配置项
  4. 四、useMutation与数据修改
    1. 4.1 基础Mutations用法
    2. 4.2 乐观更新实现
    3. 4.3 失效查询与数据同步
  5. 五、高级特性与最佳实践
    1. 5.1 查询依赖与并行查询
    2. 5.2 分页与无限滚动
    3. 5.3 SSR服务端渲染支持
    4. 5.4 React Query结合Next.js 16最佳实践
      1. 5.4.1 QueryClientProvider全局配置
      2. 5.4.2 服务端组件预取 + 客户端水合
      3. 5.4.3 静态页面预取优化
      4. 5.4.4 结合Server Actions使用Mutation
      5. 5.4.5 路由切换时的自动刷新
  6. 六、性能优化策略
    1. 6.1 结构化共享与引用稳定性
    2. 6.2 保持查询活跃
    3. 6.3 窗口焦点重新获取
  7. 七、常见应用场景总结
NEW
🚀

前端系统进阶指南

系统化学习前端知识

关注公众号

公众号:前端进价之旅

深入理解TanStack Query核心价值与实战技巧

在前端开发中,数据获取与状态管理一直是核心难题。当你的React应用需要从后端API获取数据时,你是否曾被这些问题困扰过:重复请求导致性能浪费、缓存数据难以维护、加载状态处理繁琐、乐观更新实现困难?如果你正在寻找解决方案,那么React Query(现更名为TanStack Query)正是为你而设计的。

本文将深度解读React Query的核心价值,通过大量实战代码帮助读者全面掌握这个现代React应用不可或缺的数据获取库。

一、为什么React应用需要React Query

1.1 服务端状态的特殊性

在理解React Query之前,我们需要先认识一个核心概念:服务端状态(Server State)与客户端状态(Client State)的本质区别。大多数传统状态管理库(如Redux、Zustand)在处理客户端状态时表现出色,但在处理服务端状态时却显得力不从心。这是因为服务端状态具有以下独特特性:

首先,服务端状态是远程持久化的,数据存储在你不一定拥有或控制的服务器上。其次,获取和更新数据需要异步API调用,无法像本地状态那样即时获取。第三,服务端状态是共享的,可能被其他人悄然改变。第四,如果不主动管理,服务端数据很容易变得过时。

正是这些特性,使得服务端状态管理成为前端开发中最具挑战性的领域之一。

1.2 传统方案的时代局限

在没有专门的数据获取库时,开发者通常采用以下几种方式管理服务端状态:第一种是直接在组件中useEffect配合useState,第二种是使用Redux等通用状态管理库存储异步数据,第三种是借助SWR等轻量级数据获取工具。

这些方案虽然可行,但都存在明显缺陷。手动管理数据获取意味着你需要自己处理加载状态、错误处理、缓存逻辑、重试机制等大量重复性代码。Redux虽然功能强大,但为服务端状态编写异步逻辑过于繁琐,且性能开销较大。即便是相对轻量的SWR,在复杂场景下也缺乏React Query的灵活性。

React Query的出现彻底改变了这一局面。它专门为服务端状态设计,开箱即用,拥有零配置即可使用的默认行为,同时支持高度定制以适应项目增长。

二、React Query核心概念解析

2.1 QueryKey查询键的重要性

QueryKey是React Query的核心理念之一。每个查询都需要一个唯一的键来标识数据,这个键不仅用于缓存管理,还决定了数据的依赖关系和自动刷新时机。

基础查询键的写法简单直接,例如获取待办事项列表可以使用['todos'],获取某个具体用户可以用['user', userId]。更复杂的查询可以包含多个参数,如['todos', { status: 'done', page: 1 }]。React Query会自动对查询键进行哈希处理,确保相同键的查询共享同一份缓存数据。

值得注意的是,查询键的顺序是敏感的。['todos', status, page]['todos', page, status]会被视为不同的查询,因为数组元素的顺序会影响最终的哈希值。

2.2 查询状态与获取状态

理解React Query返回的状态是正确使用库的关键。useQuery返回的结果对象包含两个维度的状态信息。

第一个维度是查询状态(status),反映数据是否存在或是否成功获取:isPending表示数据仍在加载中,isError表示查询失败并可通过error属性获取错误信息,isSuccess表示查询成功数据可通过data属性获取。

第二个维度是获取状态(fetchStatus),反映查询函数是否正在执行:fetching表示正在发起网络请求,paused表示请求因网络中断等原因暂停,idle表示当前没有进行任何请求。

这两个维度可以组合出多种状态,例如一个处于success状态且fetchStatusfetching的查询,表示当前既有缓存数据可用,又在后台进行刷新请求。这正是React Query强大的stale-while-revalidate机制的体现。

三、快速上手与基础用法

3.1 环境安装配置

React Query的安装非常简单,通过npm、pnpm或yarn均可完成:

npm install @tanstack/react-query
# 或
pnpm add @tanstack/react-query
# 或
yarn add @tanstack/react-query

安装完成后,需要在应用根组件中包裹QueryClientProvider并传入QueryClient实例:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient()

export default function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
)
}

这一步骤只需执行一次,之后整个应用中的组件都可以使用useQuery和useMutationHooks。

3.2 第一个查询用例

让我们看一个完整的查询示例,获取GitHub仓库信息:

import { useQuery } from '@tanstack/react-query'

function RepoInfo() {
const { isPending, isError, data, error } = useQuery({
queryKey: ['repoData'],
queryFn: () =>
fetch('https://api.github.com/repos/TanStack/query')
.then((res) => res.json())
})

if (isPending) return <div>加载中...</div>

if (isError) {
return <div>错误: {error.message}</div>
}

return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<div>⭐ {data.stargazers_count} Stars</div>
<div>🍴 {data.forks_count} Forks</div>
</div>
)
}

这个示例展示了useQuery的基本用法:通过queryKey指定查询标识,通过queryFn定义获取数据的异步函数。React Query会自动处理加载状态、错误处理和数据缓存。

3.3 重要默认配置项

React Query采用激进但合理的默认配置,在深入使用前理解这些默认值至关重要。

staleTime默认为0,意味着数据一旦获取就被视为过时。这会导致组件挂载时自动重新获取数据。若想避免频繁请求,可将staleTime设置为较长的时间,例如5分钟:staleTime: 5 * 60 * 1000

gcTime默认为5分钟,用于控制没有活跃观察者时缓存数据的存活时间。超过这个时间,数据将被垃圾回收。

retry默认为3次,失败的请求会自动以指数退避策略重试。这对于临时性网络错误非常有用。

refetchOnWindowFocus默认为true,当用户切换回应用窗口时会自动重新获取数据,确保展示最新的服务器状态。

四、useMutation与数据修改

4.1 基础Mutations用法

与查询不同,数据的创建、更新、删除操作应该使用useMutation。它提供了专门的状态管理来处理服务端修改:

function CreateTodo() {
const mutation = useMutation({
mutationFn: (newTodo) =>
axios.post('/api/todos', newTodo)
})

return (
<button
onClick={() => {
mutation.mutate({ title: '学习React Query', completed: false })
}}
disabled={mutation.isPending}
>
{mutation.isPending ? '创建中...' : '创建待办'}
</button>
)
}

useMutation返回的状态包括:isIdle(初始状态)、isPending(执行中)、isSuccess(成功)、isError(失败)。你可以通过这些状态向用户展示不同的UI反馈。

4.2 乐观更新实现

乐观更新是提升用户体验的关键技术,允许在服务器响应前就更新界面。React Query通过onMutateonErroronSettled三个生命周期钩子完美支持这一模式:

const queryClient = useQueryClient()

useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// 取消所有正在进行的同名查询,防止覆盖乐观更新
await queryClient.cancelQueries({ queryKey: ['todos'] })

// 快照更新前的数据,用于回滚
const previousTodos = queryClient.getQueryData(['todos'])

// 立即乐观更新缓存
queryClient.setQueryData(['todos'], (old) =>
old.map((todo) =>
todo.id === newTodo.id ? newTodo : todo
)
)

// 返回上下文对象,包含快照数据
return { previousTodos }
},
onError: (err, newTodo, context) => {
// 失败时回滚到之前的状态
queryClient.setQueryData(['todos'], context.previousTodos)
},
onSettled: () => {
// 最终总是重新获取最新数据
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
})

这段代码完整展示了乐观更新的流程:首先在onMutate中保存旧数据并更新缓存,然后如果请求失败在onError中回滚,最后无论成功失败都在onSettled中确保数据同步。

4.3 失效查询与数据同步

mutation完成后,通常需要使相关查询失效以触发数据刷新。最简单的方式是使用onSettled回调:

const mutation = useMutation({
mutationFn: addTodo,
onSettled: () => {
// 添加完成后使todos查询失效,自动触发重新获取
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
})

这种方式的优点是代码简洁,且能确保界面显示最新的服务端数据。

五、高级特性与最佳实践

5.1 查询依赖与并行查询

当某个查询需要依赖另一个查询的结果时,可以在查询函数中直接使用await获取依赖数据:

function UserProfile({ userId }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
})

const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchUserPosts(user?.id),
enabled: !!user // 只有当user数据存在时才执行
})

// 另一种方式是在queryFn中等待
const { data: userPosts } = useQuery({
queryKey: ['posts', userId],
queryFn: async () => {
const userData = await fetchUser(userId)
return fetchUserPosts(userData.id)
}
})
}

对于需要同时发起多个无关查询的场景,可以使用useQueries批量处理:

const results = useQueries({
queries: [
{ queryKey: ['users'], queryFn: fetchUsers },
{ queryKey: ['posts'], queryFn: fetchPosts },
{ queryKey: ['comments'], queryFn: fetchComments }
]
})

5.2 分页与无限滚动

React Query对分页和无限滚动提供了原生支持。分页查询的核心是将页码作为查询键的一部分:

function PaginatedList({ page }) {
const { data, isLoading } = useQuery({
queryKey: ['posts', 'list', page],
queryFn: () => fetchPosts({ page, limit: 10 })
})

// 渲染逻辑...
}

对于无限滚动,React Query提供了专门的useInfiniteQueryHook:

function InfinitePosts() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['posts', 'infinite'],
queryFn: ({ pageParam = 1 }) => fetchPosts({ page: pageParam }),
getNextPageParam: (lastPage) => lastPage.nextPage
})

return (
<div>
{data.pages.map((page) =>
page.posts.map((post) => <PostCard key={post.id} post={post} />)
)}

<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? '加载中...' : '加载更多'}
</button>
</div>
)
}

5.3 SSR服务端渲染支持

在Next.js等SSR框架中使用React Query时,需要处理服务端数据预取和客户端水合。React Query提供了HydrationBoundarydehydrate工具:

import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'

export async function getServerSideProps() {
const queryClient = new QueryClient()

await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})

return {
props: {
dehydratedState: dehydrate(queryClient)
}
}
}

export default function PostsPage({ dehydratedState }) {
return (
<HydrationBoundary state={dehydratedState}>
<PostsList />
</HydrationBoundary>
)
}

这种方案确保服务端预取的数据能够传递到客户端,避免页面加载时的闪烁问题。

5.4 React Query结合Next.js 16最佳实践

Next.js 16引入了App Router架构,React Query在其中的使用方式与传统Pages Router有所不同。下面详细讲解如何在Next.js 16中最佳实践React Query。

5.4.1 QueryClientProvider全局配置

首先需要在应用根布局中配置QueryClientProvider。为了避免服务端和客户端使用不同的实例,我们需要使用React的useState来确保客户端只创建一个QueryClient:

// app/providers.tsx
'use client'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState, type ReactNode } from 'react'

export default function Providers({ children }: { children: ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// SSR场景下,默认 staleTime 设置更长避免额外请求
staleTime: 60 * 1000,
},
},
})
)

return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}

然后在根布局中使用:

// app/layout.tsx
import Providers from './providers'

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}

5.4.2 服务端组件预取 + 客户端水合

在App Router中,我们可以在服务端组件中预取数据,然后传递给客户端组件进行水合:

// app/posts/page.tsx (服务端组件)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
import PostsList from './PostsList'
import { getPosts } from '@/api/posts'

export default async function PostsPage() {
const queryClient = new QueryClient()

await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: getPosts,
})

return (
// 将预取的数据传递给客户端组件
<HydrationBoundary state={dehydrate(queryClient)}>
<PostsList />
</HydrationBoundary>
)
}
// app/posts/PostsList.tsx (客户端组件)
'use client'

import { useQuery } from '@tanstack/react-query'
import { getPosts } from '@/api/posts'

export default function PostsList() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
})

if (isLoading) return <div>加载中...</div>
if (error) return <div>错误: {error.message}</div>

return (
<ul>
{data?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}

5.4.3 静态页面预取优化

对于博客、文档等静态内容页面,可以利用React Query的缓存策略减少服务端请求:

// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: { params: { slug: string } }) {
const queryClient = new QueryClient()

// 针对静态内容,可以设置较长的 staleTime
await queryClient.prefetchQuery({
queryKey: ['blog', params.slug],
queryFn: () => getBlogPost(params.slug),
queryOptions: {
staleTime: 60 * 60 * 1000, // 1小时内视为新鲜
},
})

return (
<HydrationBoundary state={dehydrate(queryClient)}>
<BlogPostContent slug={params.slug} />
</HydrationBoundary>
)
}

5.4.4 结合Server Actions使用Mutation

Next.js 16的Server Actions是处理数据修改的强大工具,React Query可以与其完美配合:

// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function createPost(formData: FormData) {
const title = formData.get('title')

const res = await fetch('/api/posts', {
method: 'POST',
body: JSON.stringify({ title }),
})

// 使posts查询失效,触发重新获取
revalidatePath('/posts')

return res.json()
}
// app/posts/CreatePost.tsx
'use client'

import { useMutation, useQueryClient } from '@tanstack/react-query'
import { createPost } from '@/app/actions'

export function CreatePost() {
const queryClient = useQueryClient()

const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
// 手动使缓存失效,确保列表更新
queryClient.invalidateQueries({ queryKey: ['posts'] })
},
})

return (
<form action={mutation.mutate}>
<input name="title" placeholder="输入标题" />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? '提交中...' : '创建'}
</button>
</form>
)
}

5.4.5 路由切换时的自动刷新

在Next.js App Router中,结合React Query可以轻松实现路由切换时的数据刷新:

// app/posts/[id]/page.tsx
import { getPost } from '@/api/posts'

export default async function PostPage({ params }: { params: { id: string } }) {
const queryClient = new QueryClient()

await queryClient.prefetchQuery({
queryKey: ['post', params.id],
queryFn: () => getPost(params.id),
})

return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostDetail postId={params.id} />
</HydrationBoundary>
)
}
// app/posts/[id]/PostDetail.tsx
'use client'

import { useQuery } from '@tanstack/react-query'
import { getPost } from '@/api/posts'

export default function PostDetail({ postId }: { postId: string }) {
// 当postId变化时,React Query会自动触发新的请求
const { data, isLoading } = useQuery({
queryKey: ['post', postId],
queryFn: () => getPost(postId),
})

// 渲染逻辑...
}

Next.js 16与React Query的结合,使得服务端预取、客户端水合、数据变更都变得非常优雅。这种方案既保留了服务端渲染的SEO优势和首屏加载速度,又充分利用了React Query强大的客户端状态管理能力。

六、性能优化策略

6.1 结构化共享与引用稳定性

React Query默认启用结构化共享(Structural Sharing)来优化性能。当服务端返回的数据与缓存相同时,Query会保持原有引用不变,这使得配合useMemo和useCallback使用时能有效避免不必要的重渲染:

function PostList() {
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})

// 依赖data的回调函数
const handleClick = useCallback((id) => {
console.log('Clicked:', id)
}, [])

return (
<ul>
{data?.map((post) => (
<PostItem
key={post.id}
post={post}
onClick={handleClick}
/>
))}
</ul>
)
}

在大多数场景下,默认的结构化共享已经足够高效。如果确实需要禁用此特性,可以将structuralSharing设置为false。

6.2 保持查询活跃

默认情况下,当所有使用该查询的组件都卸载后,查询会进入非活跃状态并在5分钟后被垃圾回收。但在某些场景下,我们希望查询数据继续保持活跃状态:

// 方式一:使用keepPreviousData保持上一页数据
const { data } = useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts({ page }),
placeholderData: keepPreviousData
})

// 方式二:设置较长的staleTime避免自动失效
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: Infinity // 数据永不过期,需要手动invalidate
})

6.3 窗口焦点重新获取

用户切换浏览器标签页后再返回时,自动刷新数据是一个常见需求。React Query默认启用这一行为,但可以根据场景调整:

const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
// 禁用窗口焦点刷新
refetchOnWindowFocus: false,
// 或者只在数据过时时才刷新
refetchOnWindowFocus: (query) => query.state.dataUpdatedAt > 0
})

七、常见应用场景总结

React Query几乎能解决现代React应用中的所有数据获取需求。在用户个人资料展示场景中,可以用useQuery获取用户信息并设置较长的缓存时间减少请求。在待办事项管理场景中,create、update、delete操作配合乐观更新能带来流畅的用户体验。在实时数据看板场景中,配置合适的refetchInterval实现数据定时刷新。在搜索功能实现中,结合debounce和enabled选项避免不必要的请求。

总的来说,React Query已经成为React生态中数据获取的事实标准。它不仅大幅简化了数据获取的代码量,更重要的是提供了专业级的缓存管理、错误处理和性能优化能力。无论是小型项目还是大型企业应用,React Query都能提供显著的开发体验提升和性能改进。

掌握React Query,将让你在处理服务端状态时更加得心应手,构建出更优质的React应用。

支持一下
扫一扫,支持poetries
  • 微信扫一扫
  • 支付宝扫一扫