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。它会让你的代码更加简洁、可维护,并提供出色的用户体验。别忘了查阅官方文档,那里有更多高级功能等待探索!

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

相关推荐
大怪v36 分钟前
【Virtual World 04】我们的目标,无限宇宙!!
前端·javascript·代码规范
狂炫冰美式1 小时前
不谈技术,搞点文化 🧀 —— 从复活一句明代残诗破局产品迭代
前端·人工智能·后端
xw52 小时前
npm几个实用命令
前端·npm
!win !2 小时前
npm几个实用命令
前端·npm
代码狂想家2 小时前
使用openEuler从零构建用户管理系统Web应用平台
前端
dorisrv3 小时前
优雅的React表单状态管理
前端
蓝瑟4 小时前
告别重复造轮子!业务组件多场景复用实战指南
前端·javascript·设计模式
dorisrv4 小时前
高性能的懒加载与无限滚动实现
前端
韭菜炒大葱4 小时前
别等了!用 Vue 3 让 AI 边想边说,字字蹦到你脸上
前端·vue.js·aigc
StarkCoder4 小时前
求求你,别在 Swift 协程开头写 guard let self = self 了!
前端