React 数据请求:TanStack Query

React 数据请求:TanStack Query

前置:React 路由:React Router

目标:用 Query 管理 loading / error / 缓存,而不是手写一堆 useEffect + useState。

引言

组件里的 useState 管 UI 状态;服务器上的列表 / 详情是另一类东西:要请求、等 loading、处理失败、缓存、再刷新。

手写 useEffect + fetch + 三个 useState 容易漏重试、漏竞态。TanStack Query(@tanstack/react-query)专门管这类「服务端状态」。

本篇 demo:本地 mock 待办 + 600ms 延迟,演示 loading / 列表 / 手动刷新(离线也能跑)。

动手

完整代码:react2/07-data-fetching/demo

bash 复制代码
cd react2/07-data-fetching/demo
npm install && npm run dev

打开后先看到「加载中...」,再出列表;点「重新请求」会再走一遍 delay。


1. 为什么不用纯 useEffect

手写常见形态:

tsx 复制代码
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)

useEffect(() => {
  fetch('/api/todos')
    .then((r) => r.json())
    .then(setData)
    .catch(setError)
    .finally(() => setLoading(false))
}, [])

缺什么:取消过期请求、窗口切回自动刷新、同 key 去重、失败重试、缓存共享......Query 把这些默认带上。


2. 先挂 QueryClientProvider

tsx 复制代码
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'

const client = new QueryClient()

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

useQuery 必须在 Provider 里面用。一般在应用根(App 或 main)包一层即可。


3. useQuery:声明「要这份数据」

tsx 复制代码
async function fetchTodos(): Promise<Todo[]> {
  await new Promise((r) => setTimeout(r, 600)) // 假装网络
  return MOCK_TODOS
}

function TodoList() {
  const { data, isPending, isError, error, refetch, isFetching } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  if (isPending) return <p>加载中...</p>
  if (isError) return <p className="err">失败:{(error as Error).message}</p>

  return (
    <section>
      <button type="button" onClick={() => refetch()} disabled={isFetching}>
        {isFetching ? '刷新中...' : '重新请求'}
      </button>
      <ul>
        {data.map((t) => (
          <li key={t.id}>
            <span className={t.completed ? 'done' : ''}>{t.title}</span>
          </li>
        ))}
      </ul>
    </section>
  )
}
字段 含义
queryKey 这份数据的缓存身份证;相同 key 共享缓存
queryFn 真正去拉数据的异步函数
isPending 还没有成功数据(首屏 loading)
isError / error 失败
data 成功后的结果(过了 pending/error 分支后可放心用)
refetch 手动再请求一次
isFetching 正在请求中(含后台刷新;已有数据时也可能为 true)

isPending vs isFetching:

首屏用 isPending 挡整页;点刷新时已有列表,用 isFetching 只禁用按钮 / 显示「刷新中」,别把整页清空。


4. queryKey 怎么设计

tsx 复制代码
queryKey: ['todos']           // 整份列表
queryKey: ['todo', id]        // 某一条(id 变就另缓存)
queryKey: ['todos', { q }]    // 带筛选条件

规则直觉:

  • key 变了 → 当作另一份查询
  • 同一 key → 尽量复用缓存,少打重复请求

5. 接到真实 API

把 queryFn 换成 fetch 即可,其它 UI 分支不用动:

tsx 复制代码
async function fetchTodos(): Promise<Todo[]> {
  const res = await fetch('/api/todos')
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  return res.json()
}

建议:在 queryFn 里对非 2xx 抛错 ,这样会走 isError,而不是把错误 HTML 当成 data。


对照表

概念 干什么 demo
QueryClient 缓存与调度中心 模块顶层 new QueryClient()
QueryClientProvider 注入 client 包住 TodoList
queryKey 缓存键 ['todos']
queryFn 拉数 fetchTodos(mock delay)
isPending / isError 首屏状态 加载中 / 失败文案
refetch + isFetching 手动刷新 「重新请求」按钮

常见坑

做法 问题
忘了 Provider useQuery 直接报错
queryKey 写成每次新对象却又不稳定 缓存命中乱、反复请求
只用 isFetching 做首屏 已有缓存时可能闪一下空态,语义和 isPending 不同
queryFn 失败却 return null 不 throw 不会进 isError
和 useState 里再存一份 data 双数据源,以 Query 的 data 为准

demo 在练什么

  1. Provider + useQuery 拉 mock 列表
  2. isPending / isError 分支
  3. refetch 手动刷新,用 isFetching 反馈按钮状态

自己改一改:让 fetchTodos 随机 throw new Error('网络炸了'),看错误分支;或把 key 改成 ['todos', query] 练筛选。


运行截图

下一篇

React 大型项目目录骨架

相关推荐
程序员-Benothing1 小时前
Shell脚本入门教程:Shebang、变量、注释与执行方式
前端·chrome
java_nnnn1 小时前
JavaEE进阶-JavaScript初识
开发语言·前端·javascript·java-ee·ecmascript
裕晟资质规划1 小时前
涉密场所物理隔离与技术防护体系:标准矩阵、审查校验点与常见缺陷分析
大数据·前端·网络·人工智能·经验分享
变与不变8061 小时前
JSON基础详解
前端·javascript
吴声子夜歌2 小时前
HTML——结构化微数据语言简介
前端·数据库·html
TeamDev2 小时前
JxBrowser 9.5.2 版本发布啦!
java·前端·windows·chromium·混合应用·jxbrowser·浏览器控件
鬼手点金2 小时前
Claude Code示范案例-进阶学习路径
java·服务器·前端·学习·计算机视觉·前向传播
吴声子夜歌2 小时前
HTML——Web Components开发
前端·html