React 数据请求:TanStack Query

72 阅读3分钟

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

cd react2/07-data-fetching/demo
npm install && npm run dev

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


1. 为什么不用纯 useEffect

手写常见形态:

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

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:声明「要这份数据」

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 怎么设计

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

规则直觉:

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

5. 接到真实 API

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

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手动刷新「重新请求」按钮

常见坑

做法问题
忘了 ProvideruseQuery 直接报错
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 大型项目目录骨架