TanStack React Query:现代 React 应用的数据管理利器

发布时间:2026/7/16 4:27:22

TanStack React Query:现代 React 应用的数据管理利器 1. 引言为什么需要 React Query在构建现代 React 应用时数据获取、缓存、同步和更新是开发者面临的核心挑战。传统的状态管理库如 Redux主要关注客户端状态对于服务器状态异步数据的管理往往显得笨重且模板代码繁多。这正是TanStack React Query原 React Query诞生的背景。React Query 将自己定位为“缺失的服务器状态管理库”它专门为处理异步数据而生提供了开箱即用的数据获取、缓存、后台更新、乐观更新、分页、无限加载等强大功能极大地简化了数据层逻辑。2. 核心概念2.1 Query查询Query 是 React Query 的核心抽象代表一个从服务器获取数据的异步操作。每个 Query 都有一个唯一的键key和与之关联的异步函数queryFn。import { useQuery } from tanstack/react-query; const { data, isLoading, error } useQuery({ queryKey: [todos], queryFn: () fetch(/api/todos).then(res res.json()) });Query 主要参数说明useQuery接收一个配置对象以下是常用参数及其含义queryKey必需一个数组用于唯一标识查询。React Query 根据此键进行缓存管理、数据失效和依赖查询。数组元素可以是字符串、数字、对象或嵌套数组。queryFn必需一个返回 Promise 的异步函数用于实际获取数据。函数接收一个QueryFunctionContext对象作为参数包含queryKey、pageParam用于无限查询等属性。当 queryKey 发生变化时React Query 会检查缓存并可能调用 queryFn相同的 queryKey 会复用缓存数据enabled布尔值控制查询是否自动执行。默认为true。设置为false时查询不会自动发起需要手动调用refetch。retry查询失败时的重试次数。可以是数字如3或布尔值false表示不重试。默认值为3。retryDelay重试延迟时间毫秒。可以是一个函数接收重试次数作为参数返回延迟时间。staleTime数据被视为“陈旧”的时间毫秒。在数据变为陈旧之前React Query 不会重新获取。默认值为0立即陈旧。gcTime原 cacheTime查询数据在缓存中保留的时间毫秒。当查询不再被任何组件使用时数据会在指定时间后被垃圾回收。默认值为5 * 60 * 10005分钟。refetchOnWindowFocus窗口重新获得焦点时是否重新获取数据。默认值为true。refetchOnMount组件挂载时是否重新获取数据。可选值true总是重新获取、false不重新获取、always即使数据新鲜也重新获取。refetchOnReconnect网络重新连接时是否重新获取数据。默认值为true。refetchInterval轮询间隔时间毫秒。设置为数字时会定期重新获取数据。initialData初始数据用于在首次获取数据前填充缓存。可以是具体值或返回初始数据的函数。placeholderData占位数据在查询加载时显示但不会持久化到缓存中。select一个转换函数用于在数据返回后对其进行转换。例如可以只提取数据的特定字段。onSuccess查询成功时的回调函数接收数据作为参数。onError查询失败时的回调函数接收错误对象作为参数。onSettled查询完成无论成功或失败时的回调函数接收数据和错误对象作为参数。除了这些核心参数外useQuery还返回丰富的状态信息如isLoading、isError、isSuccess、isFetching、status、error、data等以及操作方法如refetch、remove等。2.2 Mutation变更Mutation 用于创建、更新或删除服务器数据POST、PUT、DELETE 等操作。与 Query 不同Mutation 是“写”操作。import { useMutation } from tanstack/react-query; const mutation useMutation({ mutationFn: (newTodo) fetch(/api/todos, { method: POST, body: JSON.stringify(newTodo) }), onSuccess: () { // 数据提交成功后使相关的查询失效并重新获取 queryClient.invalidateQueries({ queryKey: [todos] }); } });Mutation 主要参数说明useMutation接收一个配置对象以下是常用参数及其含义mutationFn必需一个返回 Promise 的异步函数用于执行实际的变更操作如 API 调用。函数接收一个参数通常是待提交的数据。onMutate在 mutation 执行前触发的回调函数。常用于实现乐观更新保存当前状态以便回滚并立即更新 UI。函数应返回一个上下文对象供onError回滚时使用。onSuccessmutation 成功时的回调函数接收 mutation 返回的数据作为第一个参数提交的变量作为第二个参数。onErrormutation 失败时的回调函数接收错误对象作为第一个参数提交的变量作为第二个参数以及onMutate返回的上下文作为第三个参数。onSettledmutation 完成无论成功或失败时的回调函数接收数据/错误、提交的变量和上下文作为参数。retrymutation 失败时的重试次数。默认值为0不重试。retryDelay重试延迟时间毫秒。gcTime原 cacheTimemutation 结果在缓存中保留的时间毫秒。默认值为5 * 60 * 10005分钟。networkMode网络模式控制 mutation 在网络离线时的行为。可选值online默认需要网络、always即使离线也执行、offlineFirst离线优先。useMutation 返回的对象包含以下属性和方法mutate执行 mutation 的函数接收变量作为参数。mutateAsync返回 Promise 的 mutation 函数便于在 async/await 中使用。datamutation 成功返回的数据。errormutation 失败时的错误对象。isPendingmutation 是否正在执行中。isSuccessmutation 是否成功完成。isErrormutation 是否失败。isIdlemutation 是否处于空闲状态未开始执行。statusmutation 的当前状态idle、pending、success、error。reset重置 mutation 状态的方法。Mutation 与 Query 的一个重要区别是Mutation 默认不会自动执行需要手动调用mutate或mutateAsync来触发。这使得开发者可以更灵活地控制数据变更的时机例如在表单提交时调用。2.3 Query Client 与缓存Query Client 是 React Query 的中央管理器负责管理所有 Query 的缓存、后台更新和垃圾回收。缓存基于 Query Key 自动进行并支持精细化的失效策略。3. 核心特性与优势3.1 自动缓存与后台更新React Query 会自动缓存查询结果。当组件重新挂载或窗口重新聚焦时它会先在后台用旧数据渲染保证快速响应然后默默发起新的网络请求来更新数据最后用新数据重新渲染。这个过程对用户是无感的。3.2 乐观更新在执行 Mutation如提交表单时可以先假设操作成功立即更新 UI然后再发送请求。如果请求失败则自动回滚 UI 状态。这极大地提升了用户体验。const mutation useMutation({ mutationFn: updateTodo, // 在 mutation 执行前触发 onMutate: async (newTodo) { // 取消任何正在进行的 todos 查询以免覆盖我们的乐观更新 await queryClient.cancelQueries({ queryKey: [todos] }); // 保存前一个状态以便回滚 const previousTodos queryClient.getQueryData([todos]); // 乐观更新缓存 queryClient.setQueryData([todos], old [...old, newTodo]); // 返回一个上下文对象其中包含前一个状态用于 onError 回滚 return { previousTodos }; }, // 如果失败使用 onMutate 返回的上下文回滚 onError: (err, newTodo, context) { queryClient.setQueryData([todos], context.previousTodos); }, // 无论成功或失败总是在之后重新获取数据以确保一致性 onSettled: () { queryClient.invalidateQueries({ queryKey: [todos] }); }, });3.3 分页与无限加载React Query 原生支持分页查询useQuery和无限滚动/加载更多useInfiniteQuery处理起来非常优雅。这两种模式都基于相同的缓存和状态管理机制但提供了不同的 API 来适应不同的 UI 交互模式。3.3.1 分页查询useQuery分页查询适用于传统的分页器 UI用户通过点击页码按钮来切换页面。使用useQuery时只需将页码作为queryKey的一部分React Query 就会自动为每个页面创建独立的缓存条目。import { useQuery } from tanstack/react-query; import { useState } from react; function TodoListPaged({ initialPage 1, pageSize 10 }) { const [page, setPage] useState(initialPage); const { data, isLoading, error, isFetching } useQuery({ queryKey: [todos, { page, pageSize }], // 页码和页大小作为查询键的一部分 queryFn: async () { const response await fetch( https://jsonplaceholder.typicode.com/todos?_page${page}_limit${pageSize} ); if (!response.ok) { throw new Error(Network response was not ok); } // 假设 API 返回 { data: [...], total: 100 } const todos await response.json(); const total parseInt(response.headers.get(X-Total-Count) || 0); return { todos, total }; }, keepPreviousData: true, // 关键切换页面时保留上一页数据避免 UI 闪烁 }); if (isLoading) return divLoading.../div; if (error) return divError: {error.message}/div; const totalPages Math.ceil(data.total / pageSize); return ( div div {data.todos.map(todo ( div key{todo.id}{todo.title}/div ))} /div div button disabled{page 1} onClick{() setPage(page - 1)} 上一页 /button span第 {page} 页 / 共 {totalPages} 页/span button disabled{page totalPages} onClick{() setPage(page 1)} 下一页 /button /div {isFetching div正在获取新数据.../div} /div ); } export default TodoListPaged;关键参数说明keepPreviousData设置为true时当查询键变化如切换页面导致新查询开始时React Query 会继续显示上一次成功获取的数据直到新数据到达。这能避免页面切换时的 UI 闪烁提供更流畅的用户体验。staleTime可以适当设置较长的过期时间如 30 秒避免用户在分页间快速切换时频繁重新获取数据。3.3.2 无限滚动/加载更多useInfiniteQuery无限滚动适用于社交媒体、新闻流等需要连续加载更多内容的场景。useInfiniteQuery专门为此设计它维护一个页面列表并提供了加载下一页的便捷方法。import { useInfiniteQuery } from tanstack/react-query; import { useEffect, useRef } from react; function TodoListInfinite() { const loadMoreRef useRef(null); const { data, error, fetchNextPage, hasNextPage, isFetchingNextPage, status, } useInfiniteQuery({ queryKey: [todos, infinite], queryFn: async ({ pageParam 1 }) { const pageSize 10; const response await fetch( https://jsonplaceholder.typicode.com/todos?_page${pageParam}_limit${pageSize} ); if (!response.ok) { throw new Error(Network response was not ok); } const todos await response.json(); const total parseInt(response.headers.get(X-Total-Count) || 0); const hasMore pageParam * pageSize total; return { todos, nextPage: hasMore ? pageParam 1 : undefined }; }, getNextPageParam: (lastPage) lastPage.nextPage, initialPageParam: 1, }); // 自动加载更多 - 当滚动到底部时 useEffect(() { const observer new IntersectionObserver( (entries) { if (entries[0].isIntersecting hasNextPage !isFetchingNextPage) { fetchNextPage(); } }, { threshold: 1.0 } ); if (loadMoreRef.current) { observer.observe(loadMoreRef.current); } return () observer.disconnect(); }, [fetchNextPage, hasNextPage, isFetchingNextPage]); if (status pending) return divLoading.../div; if (status error) return divError: {error.message}/div; return ( div {data.pages.map((page, pageIndex) ( div key{pageIndex} {page.todos.map(todo ( div key{todo.id} style{{ padding: 10px, borderBottom: 1px solid #eee }} {todo.title} /div ))} /div ))} {/* 自动加载触发器 */} div ref{loadMoreRef} style{{ height: 20px }} / {/* 手动加载按钮 */} button onClick{() fetchNextPage()} disabled{!hasNextPage || isFetchingNextPage} style{{ margin: 20px 0, padding: 10px 20px }} {isFetchingNextPage ? 加载中... : hasNextPage ? 加载更多 : 没有更多了} /button /div ); } export default TodoListInfinite;核心概念解析pageParamqueryFn接收一个包含pageParam的对象表示当前要获取的页码或游标。getNextPageParam一个函数接收最后一页的数据返回下一页的pageParam。如果返回undefined表示没有更多数据。data.pages一个数组每个元素代表一页的数据。fetchNextPage加载下一页的函数。hasNextPage布尔值表示是否还有更多数据可加载。isFetchingNextPage布尔值表示是否正在加载下一页。3.3.3 两种模式的对比与选择特性分页查询useQuery无限滚动useInfiniteQuery适用场景传统分页器、表格分页、明确页码的导航社交媒体动态、新闻流、聊天记录、连续加载内容数据组织每页独立缓存通过 queryKey 区分多页数据组织为 pages 数组统一管理UI 交互点击页码/按钮切换页面滚动到底部自动加载或点击加载更多缓存策略每页独立缓存可设置 keepPreviousData 避免闪烁所有已加载页面统一缓存支持增量加载API 复杂度简单与普通查询相同稍复杂需要理解 pages 结构和分页参数最佳实践建议对于管理后台、数据表格等需要精确导航的场景使用useQuery分页。对于内容流、社交动态等需要连续浏览的场景使用useInfiniteQuery。无论使用哪种模式都要合理设置staleTime和gcTime平衡数据新鲜度和性能。考虑使用prefetchQuery预加载下一页数据提升用户体验。3.4 开发体验提升内置的 Devtools 可以可视化地查看所有 Query 和 Mutation 的状态、缓存内容方便调试。4. 实战构建一个待办事项应用让我们通过一个简单的待办事项Todo应用来体验 React Query 的核心用法。4.1 项目初始化与配置# 创建 React 应用 npx create-react-app react-query-demo cd react-query-demo # 安装 React Query npm install tanstack/react-query在应用入口文件如index.js或App.js中用QueryClientProvider包裹你的应用。// index.js import React from react; import ReactDOM from react-dom/client; import { QueryClient, QueryClientProvider } from tanstack/react-query; import App from ./App; // 创建一个 QueryClient 实例 const queryClient new QueryClient(); const root ReactDOM.createRoot(document.getElementById(root)); root.render( React.StrictMode QueryClientProvider client{queryClient} App / /QueryClientProvider /React.StrictMode );4.2 获取待办事项列表// TodoList.js import { useQuery } from tanstack/react-query; function TodoList() { const { data: todos, isLoading, error } useQuery({ queryKey: [todos], queryFn: async () { const response await fetch(https://jsonplaceholder.typicode.com/todos?_limit5); if (!response.ok) { throw new Error(Network response was not ok); } return response.json(); } }); if (isLoading) return divLoading.../div; if (error) return divError: {error.message}/div; return ( ul {todos.map(todo ( li key{todo.id} input typecheckbox checked{todo.completed} readOnly / {todo.title} /li ))} /ul ); }4.3 添加新的待办事项// AddTodo.js import { useMutation, useQueryClient } from tanstack/react-query; import { useState } from react; function AddTodo() { const [title, setTitle] useState(); const queryClient useQueryClient(); const mutation useMutation({ mutationFn: (newTodo) fetch(https://jsonplaceholder.typicode.com/todos, { method: POST, body: JSON.stringify(newTodo), headers: { Content-type: application/json; charsetUTF-8, }, }).then((response) response.json()), onSuccess: (newTodo) { // 乐观更新直接更新缓存 queryClient.setQueryData([todos], oldTodos [...oldTodos, newTodo]); setTitle(); // 清空输入框 }, }); const handleSubmit (e) { e.preventDefault(); if (!title.trim()) return; mutation.mutate({ title, completed: false, userId: 1, }); }; return ( form onSubmit{handleSubmit} input typetext value{title} onChange{(e) setTitle(e.target.value)} placeholderAdd a new todo... / button typesubmit disabled{mutation.isLoading} {mutation.isLoading ? Adding... : Add Todo} /button /form ); }5. 最佳实践与常见陷阱5.1 设计稳定的 Query KeyQuery Key 不仅是缓存的标识也用于依赖查询和手动失效。建议使用数组形式并包含所有影响查询结果的变量。// 好包含所有依赖 useQuery({ queryKey: [todos, { status: active, page: currentPage }], queryFn: fetchTodos, }); // 避免过于简单无法区分不同状态或页面的查询 useQuery({ queryKey: [todos], queryFn: fetchTodos, });5.2 合理设置缓存时间与垃圾回收通过QueryClient的默认配置或每个查询的选项可以控制缓存行为。const queryClient new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, // 数据在5分钟内被认为是“新鲜”的不会重新获取 gcTime: 10 * 60 * 1000, // 不活动的缓存数据在10分钟后被垃圾回收 retry: 2, // 失败重试2次 }, }, });5.3 避免滥用 refetchOnWindowFocus窗口聚焦时重新获取数据是 React Query 的默认行为这能保证数据新鲜度。但在某些场景如实时仪表盘、频繁更新的数据源下可能造成过多请求可以考虑适当延长staleTime或关闭此选项。5.4 使用 Prefetching 提升用户体验在用户可能访问下一页或下一个视图前预先获取数据并存入缓存。// 在用户悬停在链接上时预取下一页数据 const onHoverNextPage () { queryClient.prefetchQuery({ queryKey: [todos, { page: currentPage 1 }], queryFn: () fetchTodos(currentPage 1), }); };6. 总结TanStack React Query通过将服务器状态管理与客户端状态管理分离为 React 应用的数据层带来了革命性的简化。它解决了缓存、后台更新、乐观更新等常见痛点让开发者能更专注于业务逻辑而非数据同步的模板代码。对于大多数涉及服务器数据交互的 React 应用React Query 都应被视为首选的数据获取库。它不仅能提升开发效率更能通过智能的缓存和更新策略最终为用户带来更流畅、更快速的体验。进一步学习资源官方文档GitHub 仓库官方示例

相关新闻