React Query入门指南:简化React应用中的数据获取

文章目录

引言

前端开发中,数据获取一直是个老大难问题!!!尤其在React应用中,管理服务器状态、处理加载状态、错误处理以及缓存等都需要大量样板代码。很多开发者(包括我)都曾经被这些重复性工作折磨过...

React Query就是为解决这些痛点而生的强大开源库。它自称是"React缺失的数据获取库",而且这个说法一点都不夸张(认真脸)。今天我们就来一探究竟,看看这个库到底能帮我们解决什么问题,以及如何上手使用它。

为什么需要React Query?

在深入学习之前,我们先思考一下:传统的React数据获取方式有什么问题?

jsx 复制代码
// 传统数据获取方式
function ProductList() {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('/api/products')
      .then(res => res.json())
      .then(data => {
        setData(data);
        setIsLoading(false);
      })
      .catch(err => {
        setError(err);
        setIsLoading(false);
      });
  }, []);

  if (isLoading) return <p>加载中...</p>;
  if (error) return <p>出错了: {error.message}</p>;

  return (
    <ul>
      {data.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

看着挺简单,但随着应用复杂度增加,我们会遇到以下问题:

  1. 大量重复代码 - 每个组件都需要处理加载、错误、数据存储逻辑
  2. 缓存管理困难 - 何时刷新数据?如何避免重复请求?
  3. 数据同步 - 多个组件使用相同数据时如何保持同步?
  4. 分页/无限滚动 - 实现起来特别繁琐
  5. 乐观更新 - 提升用户体验的重要技术,但实现复杂

React Query几乎解决了以上所有问题(不是吹!)。它提供了一套声明式API,让数据获取变得简单而强大。

安装配置

首先,我们需要安装React Query:

bash 复制代码
# npm
npm install @tanstack/react-query

# yarn
yarn add @tanstack/react-query

# pnpm
pnpm add @tanstack/react-query

然后,在应用根部设置QueryClient和Provider:

jsx 复制代码
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

// 创建一个client
const queryClient = new QueryClient()

function App() {
  return (
    // 提供client给应用
    <QueryClientProvider client={queryClient}>
      {/* 应用的其他部分 */}
      <TodoList />
      
      {/* 开发工具(可选但超有用!) */}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}

准备工作完成,现在可以开始使用React Query了!

基础概念

React Query的核心概念非常简单:

  • Queries: 获取数据(GET请求)
  • Mutations: 修改数据(POST, PUT, DELETE等请求)
  • Query Invalidation: 使缓存失效,触发重新获取

先来看看最基础的使用方式:

使用useQuery获取数据

jsx 复制代码
import { useQuery } from '@tanstack/react-query'

function TodoList() {
  const { isLoading, error, data } = useQuery({
    queryKey: ['todos'],
    queryFn: () => fetch('/api/todos').then(res => res.json())
  })

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

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

就这么简单!但别被这表面的简单所迷惑,useQuery做了很多幕后工作:

  • 自动处理加载和错误状态
  • 缓存查询结果
  • 自动重试失败的请求
  • 支持轮询和定期刷新
  • 窗口聚焦时自动重新获取数据
  • 支持分页和无限滚动

queryKey的重要性

queryKey是React Query的核心概念(超级重要)。它不仅用于标识查询,还决定了何时重新获取数据。

jsx 复制代码
// 基本查询
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

// 带参数的查询
useQuery({ 
  queryKey: ['todos', { status, page }], 
  queryFn: () => fetchTodos(status, page) 
})

当依赖项(如status或page)变化时,React Query会自动重新获取数据。太智能了!

实用技巧与进阶用法

数据转换

有时API返回的数据格式可能不是组件需要的,我们可以使用select选项进行转换:

jsx 复制代码
const { data: formattedTodos } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  select: (data) => data.map(todo => ({
    ...todo,
    formattedDate: new Date(todo.createdAt).toLocaleDateString()
  }))
})

依赖查询

某些查询可能依赖于其他查询的结果:

jsx 复制代码
// 先获取用户
const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId)
})

// 再根据用户获取其待办事项
const { data: todos } = useQuery({
  queryKey: ['todos', user?.id],
  queryFn: () => fetchTodosByUser(user.id),
  // 只有当用户数据存在时才执行
  enabled: !!user
})

这种方式非常适合处理有依赖关系的API调用。

使用useMutation修改数据

对于POST、PUT、DELETE等请求,我们使用useMutation:

jsx 复制代码
import { useMutation, useQueryClient } from '@tanstack/react-query'

function AddTodo() {
  const queryClient = useQueryClient()
  const [title, setTitle] = useState('')
  
  const mutation = useMutation({
    mutationFn: (newTodo) => {
      return fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo)
      })
    },
    onSuccess: () => {
      // 成功后使相关查询失效,触发重新获取
      queryClient.invalidateQueries({ queryKey: ['todos'] })
      setTitle('')
    }
  })
  
  return (
    <form onSubmit={(e) => {
      e.preventDefault()
      mutation.mutate({ title })
    }}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
      />
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? '添加中...' : '添加待办'}
      </button>
    </form>
  )
}

乐观更新

乐观更新是提升用户体验的重要技术,通过假设请求会成功,立即更新UI:

jsx 复制代码
const queryClient = useQueryClient()

const mutation = 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'] })
  }
})

无限滚动/加载更多

React Query让实现无限滚动变得超级简单:

jsx 复制代码
import { useInfiniteQuery } from '@tanstack/react-query'

function InfiniteTodos() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage
  } = useInfiniteQuery({
    queryKey: ['todos', 'infinite'],
    queryFn: ({ pageParam = 1 }) => fetchTodoPage(pageParam),
    getNextPageParam: (lastPage) => lastPage.nextPage || undefined
  })

  return (
    <div>
      {data?.pages.map((page, i) => (
        <React.Fragment key={i}>
          {page.todos.map(todo => (
            <div key={todo.id}>{todo.title}</div>
          ))}
        </React.Fragment>
      ))}
      
      <button
        onClick={() => fetchNextPage()}
        disabled={!hasNextPage || isFetchingNextPage}
      >
        {isFetchingNextPage
          ? '加载更多...'
          : hasNextPage
          ? '加载更多'
          : '没有更多数据了'}
      </button>
    </div>
  )
}

性能优化技巧

React Query已经内置了很多性能优化,但了解这些选项可以让你更好地调整:

缓存时间与失效时间

jsx 复制代码
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // 数据缓存时间(默认5分钟)
      gcTime: 1000 * 60 * 5,
      // 数据被认为是新鲜的时间(默认0)
      staleTime: 1000 * 60,
      // 重试次数
      retry: 3,
      // 窗口重新聚焦时重新获取(默认true)
      refetchOnWindowFocus: true,
    },
  },
})

手动控制重新获取

有时你可能想手动控制数据刷新:

jsx 复制代码
function TodoList() {
  const { data, refetch } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    // 禁用自动重新获取
    refetchOnWindowFocus: false,
    refetchOnMount: false,
    staleTime: Infinity
  })

  return (
    <div>
      <button onClick={() => refetch()}>刷新数据</button>
      {/* 渲染数据 */}
    </div>
  )
}

实际项目中的最佳实践

在我的经验中,以下做法能让React Query在大型项目中更好用:

自定义Hooks

将查询逻辑封装到自定义hooks中:

jsx 复制代码
// hooks/useTodos.js
export function useTodos(filters) {
  return useQuery({
    queryKey: ['todos', filters],
    queryFn: () => fetchTodos(filters)
  })
}

export function useAddTodo() {
  const queryClient = useQueryClient()
  
  return useMutation({
    mutationFn: addTodo,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    }
  })
}

// 使用
function TodoApp() {
  const { data, isLoading } = useTodos({ status: 'active' })
  const addTodoMutation = useAddTodo()
  
  // ...使用数据和mutation
}

预取数据

对于重要页面,可以预先获取数据提升用户体验:

jsx 复制代码
// 在路由变化或用户hover某个链接时预取
function prefetchTodoPage(todoId) {
  queryClient.prefetchQuery({
    queryKey: ['todo', todoId],
    queryFn: () => fetchTodoById(todoId)
  })
}

全局错误处理

设置全局错误处理逻辑:

jsx 复制代码
const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) => {
      // 处理所有查询错误
      if (error.status === 401) {
        // 重定向到登录页
      }
      
      console.error(`查询失败: ${query.queryKey}`);
    },
  }),
  mutationCache: new MutationCache({
    onError: (error) => {
      // 处理所有mutation错误
      toast.error(`操作失败: ${error.message}`)
    }
  }),
})

何时不使用React Query

虽然React Query很强大,但并非所有场景都适合:

  1. 客户端状态管理 - 对于纯本地状态,Redux或Context可能更合适
  2. 极简应用 - 如果应用非常简单,可能不值得引入额外库
  3. 不需要缓存的API调用 - 某些一次性操作可能不需要React Query

结语

React Query彻底改变了我处理服务器数据的方式。它优雅地解决了React应用中数据获取的绝大多数问题,让我可以专注于业务逻辑而不是重复编写数据获取的样板代码。

我强烈建议任何中大型React应用都考虑使用React Query。它会让你的代码更加简洁、可维护,并提供出色的用户体验。别忘了查阅官方文档,那里有更多高级功能等待探索!

希望这篇入门指南对你有所帮助。编码愉快!

相关推荐
知识分享小能手2 小时前
微信小程序入门学习教程,从入门到精通,微信小程序开发进阶(7)
前端·javascript·学习·程序人生·微信小程序·小程序·vue3
sophie旭4 小时前
一道面试题,开始性能优化之旅(8)-- 构建工具和性能
前端·面试·性能优化
市民中心的蟋蟀4 小时前
第三章 钩入React 【上】
前端·react.js·架构
Holin_浩霖4 小时前
为什么typeof null 返回 "object" ?
前端
PanZonghui5 小时前
Zustand 实战指南:从基础到高级,构建类型安全的状态管理
前端·react.js
PanZonghui5 小时前
Vite 构建优化实战:从配置到落地的全方位性能提升指南
前端·react.js·vite
_extraordinary_5 小时前
Java Linux --- 基本命令,部署Java web程序到线上访问
java·linux·前端
用户1456775610375 小时前
推荐一个我私藏的电脑神器:小巧、无广、功能强到离谱
前端
用户1456775610375 小时前
终于找到了!一个文件搞定PDF阅读
前端