React Query:彻底改变数据交互模式,为什么它正在取代Redux的异步场景?

363 阅读9分钟

“你是否还在为这些数据问题熬夜?”

  • 反复编写 useEffect 处理加载/错误状态,代码冗余率高达60%
  • 页面跳转后返回,数据明明已更新却显示陈旧内容
  • 同一接口被多个组件重复调用,性能急剧下降
  • 全局状态库中混杂着服务器数据与UI状态,维护如走钢丝

👉 这正是React Query要解决的革命性问题

传统数据获取 vs React Query

传统方式:手动管理的地狱

 // 痛点代码:16行状态管理 + 易错的依赖项
function usePhotos() {
    const [loading, setLoading] = useState(false);
    const [photos, setPhotos] = useState([]);
    const [error, setError] = useState(null);
    useEffect(() => {
        setLoading(true);
        photoAPI.getAll(1).
        then(
            data => setPhotos(data)
        ).catch(
            e => setError(e)).finally(() => setLoading(false)
        );
    }, []);
    // ❗ 缺少刷新依赖项可能导致数据过期
    return { loading, photos, error };
}

⚠️ 隐藏问题:

  1. 无自动缓存策略,后退页面时数据重置
  2. 多组件同时调用时产生重复请求
  3. 需要手动实现轮询/懒加载等高级功能
  4. 可能会不小心拿到“过时”的数据
  5. 数据缓存
  6. 性能优化,如分页和延迟加载数据

React Query 方案:声明式优雅

 // 解决方案:1行核心代码获得完整能力
function usePhotos() {
    return useQuery({ 
        queryKey: ['photos'], 
        queryFn: () => photoAPI.getAll(1),
        staleTime: 5 * 60 * 1000 
        // ⚡自动5分钟缓存
    });
}

🚀 自动获得:

  • 智能缓存(内存 & 持久化可选)
  • 重复请求自动合并(Deduplication)
  • 后台静默刷新(Stale-While-Revalidate)
  • 滚动恢复时的即时数据展示

React Query Example

import {
  useQuery,
  useMutation,
  useQueryClient,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
import { getTodos, postTodo } from '../my-api'

// Create a client
const queryClient = new QueryClient()

function App() {
  return (
    // Provide the client to your App
    <QueryClientProvider client={queryClient}>
      <Todos />
    </QueryClientProvider>
  )
}

function Todos() {
  // Access the client
  const queryClient = useQueryClient()

  // Queries
  const query = useQuery({ queryKey: ['todos'], queryFn: getTodos })

  // Mutations
  const mutation = useMutation({
    mutationFn: postTodo,
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })

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

      <button
        onClick={() => {
          mutation.mutate({
            id: Date.now(),
            title: 'Do Laundry',
          })
        }}
      >
        Add Todo
      </button>
    </div>
  )
}

render(<App />, document.getElementById('root'))

核心概念

query的核心概念就是下面三个:queriesmutationsquery invalidation,让我来向你一一介绍:

Queries(查询)

Queries是对与唯一键绑定的异步数据源的声明性依赖项。Queries可以与任何基于 Promise 的方法(包括 GET 和 POST 方法)一起使用,以从服务器获取数据。如果您的方法修改了服务器上的数据,我们建议改用 Mutations

要在组件或自定义 hook 中订阅查询,使用以下命令调用 useQuery hook:查询的唯一键(在内部用于在整个应用程序中重新获取、缓存和共享查询)&&一个函数(返回一个 Promise,该 Promise:解析数据或引发错误)。

import { useQuery } from '@tanstack/react-query'
function App() {  
    const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })
}

result 对象包含一些非常重要的状态,query在任何给定时刻只能处于以下状态之一:

  • isPendingstatus === 'pending' - 查询尚无数据
  • isErrorstatus === 'error' - 查询遇到错误
  • isSuccessstatus === 'success' - 查询成功且数据可用

除了这些主要状态之外,根据查询的状态,还可以获得更多信息:

  • error - 如果查询处于 isError 状态,则可通过 error 属性获取错误。
  • data - 如果查询处于 isSuccess 状态,则可通过 data 属性获取数据。
  • isFetching - 在任何状态下,如果查询在任何时间(包括后台重新获取)都在提取,则 isFetching 将为 true。

对于大多数查询,通常只需检查 isPending 状态,然后检查 isError 状态,最后假设数据可用并呈现成功状态:

function Todos() {
  const { isPending, isError, data, error } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodoList,
  })
  if (isPending) {
    return <span>Loading...</span>
  }
  if (isError) {
    return <span>Error: {error.message}</span>
  }
  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}

查询键: 查询键必须是顶层的 Array,可以像具有单个字符串的 Array 一样简单,也可以像包含许多字符串和嵌套对象的 Array 一样复杂。只要查询键是可序列化的,并且对于查询数据是唯一的,您就可以使用它!

Mutations(突变)

Mutations通常用于创建/更新/删除数据或执行服务器副作用。为此,TanStack Query 导出了一个 useMutation 钩子。

下面是一个向服务器添加新 todo 的 mutation 示例:

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

  return (
    <div>
      {mutation.isPending ? (
        'Adding todo...'
      ) : (
        <>
          {mutation.isError ? (
            <div>An error occurred: {mutation.error.message}</div>
          ) : null}

          {mutation.isSuccess ? <div>Todo added!</div> : null}

          <button
            onClick={() => {
              mutation.mutate({ id: new Date(), title: 'Do Laundry' })
            }}
          >
            Create Todo
          </button>
        </>
      )}
    </div>
  )
}

在任何给定时刻,更改只能处于以下状态之一:

  • isIdlestatus === 'idle' - 变更当前处于空闲状态或处于全新/重置状态
  • isPendingstatus === 'pending' - 更改当前正在运行
  • isErrorstatus === 'error' - 更改遇到错误
  • isSuccessstatus === 'success' - 更改成功且更改数据可用

除了这些主要状态之外,根据更改的状态,还可以获得更多信息:

  • error - 如果更改处于 error 状态,则可通过 error 属性获取错误。
  • data - 如果更改处于成功状态,则可通过 data 属性获取数据。

在上面的示例中,您还看到,您可以通过使用单个变量或对象调用 mutate 函数来将变量传递给 mutations 函数。

重要提示:mutate 函数是一个异步函数,这意味着你不能直接在 React 16 及更早版本的事件回调中使用它。如果需要访问 onSubmit 中的事件,则需要将 mutate 包装在另一个函数中。

// This will not work in React 16 and earlier
const CreateTodo = () => {
  const mutation = useMutation({
    mutationFn: (event) => {
      event.preventDefault()
      return fetch('/api', new FormData(event.target))
    },
  })

  return <form onSubmit={mutation.mutate}>...</form>
}

// This will work
const CreateTodo = () => {
  const mutation = useMutation({
    mutationFn: (formData) => {
      return fetch('/api', formData)
    },
  })
  const onSubmit = (event) => {
    event.preventDefault()
    mutation.mutate(new FormData(event.target))
  }

  return <form onSubmit={onSubmit}>...</form>
}

Query Invalidation(查询失效)

等待查询变得过时后再再次获取它们并不总是有效的,尤其是当您知道查询的数据由于用户执行的某些作而过时时。为此,QueryClient 有一个 invalidateQueries 方法,它允许您智能地将查询标记为过时,并可能重新获取它们!

// Invalidate every query in the cache
queryClient.invalidateQueries()
// Invalidate every query with a key that starts with `todos`
queryClient.invalidateQueries({ queryKey: ['todos'] })

当查询因 invalidateQueries 而失效时,会发生两种情况:

  • 它被标记为过时。这种过时的状态会覆盖 useQuery 或相关钩子中使用的任何 staleTime 配置
  • 如果查询当前正在通过 useQuery 或相关钩子渲染,它也将在后台重新获取

使用 invalidateQueries 进行查询匹配

当使用 invalidateQueries 和 removeQueries(以及其他支持部分查询匹配的 API)时,您可以按前缀匹配多个查询,或者获得非常具体并匹配确切的查询。

在此示例中,我们可以使用 todos 前缀使查询键中以 todos 开头的任何查询无效:

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

// Get QueryClient from the context
const queryClient = useQueryClient()

queryClient.invalidateQueries({ queryKey: ['todos'] })

// Both queries below will be invalidated
const todoListQuery = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodoList,
})
const todoListQuery = useQuery({
  queryKey: ['todos', { page: 1 }],
  queryFn: fetchTodoList,
})

Query 运行流程

React Query 的生命周期始于组件挂载或依赖项变化时,首先会检查缓存中是否存在与当前查询键(Query Key)匹配且未过期的数据。如果存在有效缓存,则立即将缓存数据返回给组件使用,此时组件会直接进入 isSuccess 状态,用户界面得以快速呈现,无需等待网络请求。这一缓存优先策略是 React Query 高性能的核心设计之一,它避免了不必要的网络请求,显著提升了用户体验。

如果缓存不存在或已过期,React Query 会发起一个新的网络请求。在请求发出后,查询状态会立即标记为 isPending,此时组件可以展示加载状态以提供用户反馈。请求发出后,系统会进入等待响应阶段,这个过程可能成功也可能失败。如果请求成功,返回的数据会被存入缓存,并将查询状态更新为 isSuccess,触发组件重新渲染以展示最新数据。如果请求失败,错误信息会被记录,状态标记为 isError,组件可以据此展示错误提示,同时 React Query 会根据配置的 retry 策略自动尝试重新请求。

当数据成功获取并进入缓存后,便开始受到 staleTime 配置的控制。staleTime 决定了数据在缓存中的保鲜期,默认值为 0 表示获取后立即视为过期。一旦数据过期,查询会被标记为 stale 状态,但这是一个内部状态,不会立即影响用户体验。此时如果相关组件仍在渲染,React Query 会在后台静默发起新的请求来更新数据,这就是所谓的 Stale-While-Revalidate 策略。这种设计使得用户总能立即看到数据(即便是过期的),同时在后台悄无声息地保持数据新鲜度。

对于不再渲染的组件,其查询数据会继续保留在缓存中,直到垃圾回收机制根据 gcTime 的设置将其清除。gcTime 默认为 5 分钟,控制着未使用数据在内存中的保留时间。这个机制有效平衡了内存使用和数据复用需求,避免了内存无限制增长。

除了自动更新外,React Query 还提供了强大的手动干预能力。开发者可以主动调用 invalidateQueries 来标记特定查询为过期,强制触发后台更新。也可以直接使用 setQueryData 修改本地缓存,这在实现乐观更新时特别有用。这些手动操作与自动机制完美配合,为开发者提供了精细的控制能力,同时又不失框架的自动化优势。

Query 的替代性

TanStack Query 会取代 Redux 或其他全局状态管理器吗?

场景

Redux适用场景
  • 跨组件共享的UI状态(如主题、侧边栏开关)
  • 需要时间旅行调试的复杂交互流程
  • 客户端计算的派生状态(如购物车总价)
React Query适用场景
  • 所有异步服务端状态(GET/POST请求)
  • 需要缓存策略的数据(如用户信息、配置项)
  • 高频更新的实时数据(股票行情、聊天消息)
 // Redux管理客户端状态
const theme = useSelector(state => state.ui.theme);
// React Query管理服务端状态
const { data: user } = useQuery(['user'], fetchUser);

性能对比表格

特性传统方式Redux ThunkReact Query
自动缓存
重复请求合并
后台自动刷新
内存占用极低
代码量(相同功能)100%150%30%

实战场景:电商后台管理

场景1:商品列表页
  • React Query自动缓存分页数据
  • 切换页面时保留滚动位置,瞬时展示缓存
  • 后台每5分钟静默刷新库存信息
场景2:订单提交
  • 使用useMutation处理下单请求
  • 乐观更新显示成功状态,无需等待接口返回
  • 提交后自动刷新用户余额查询

结语

React Query不是要取代Redux,而是将开发者从繁琐的异步逻辑中解放。当你的应用有超过30%的状态来自服务端时,它就是你的下一个必备武器。

🌟 现在就开始:用10%的代码量,获得200%的性能提升!