React Query入门指南:简化React应用中的数据获取
文章目录
- 引言
- 为什么需要React Query?
- 安装配置
- 基础概念
- 使用useQuery获取数据
- queryKey的重要性
- 实用技巧与进阶用法
- 数据转换
- 依赖查询
- 使用useMutation修改数据
- 乐观更新
- 无限滚动/加载更多
- 性能优化技巧
- 缓存时间与失效时间
- 手动控制重新获取
- 实际项目中的最佳实践
- 自定义Hooks
- 预取数据
- 全局错误处理
- 何时不使用React Query
- 结语
引言
前端开发中,数据获取一直是个老大难问题!!!尤其在React应用中,管理服务器状态、处理加载状态、错误处理以及缓存等都需要大量样板代码。很多开发者(包括我)都曾经被这些重复性工作折磨过…
React Query就是为解决这些痛点而生的强大开源库。它自称是"React缺失的数据获取库",而且这个说法一点都不夸张(认真脸)。今天我们就来一探究竟,看看这个库到底能帮我们解决什么问题,以及如何上手使用它。
为什么需要React Query?
在深入学习之前,我们先思考一下:传统的React数据获取方式有什么问题?
// 传统数据获取方式
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>);
}
看着挺简单,但随着应用复杂度增加,我们会遇到以下问题:
- 大量重复代码 - 每个组件都需要处理加载、错误、数据存储逻辑
- 缓存管理困难 - 何时刷新数据?如何避免重复请求?
- 数据同步 - 多个组件使用相同数据时如何保持同步?
- 分页/无限滚动 - 实现起来特别繁琐
- 乐观更新 - 提升用户体验的重要技术,但实现复杂
React Query几乎解决了以上所有问题(不是吹!)。它提供了一套声明式API,让数据获取变得简单而强大。
安装配置
首先,我们需要安装React Query:
# npm
npm install @tanstack/react-query# yarn
yarn add @tanstack/react-query# pnpm
pnpm add @tanstack/react-query
然后,在应用根部设置QueryClient和Provider:
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获取数据
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的核心概念(超级重要)。它不仅用于标识查询,还决定了何时重新获取数据。
// 基本查询
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })// 带参数的查询
useQuery({ queryKey: ['todos', { status, page }], queryFn: () => fetchTodos(status, page)
})
当依赖项(如status或page)变化时,React Query会自动重新获取数据。太智能了!
实用技巧与进阶用法
数据转换
有时API返回的数据格式可能不是组件需要的,我们可以使用select
选项进行转换:
const { data: formattedTodos } = useQuery({queryKey: ['todos'],queryFn: fetchTodos,select: (data) => data.map(todo => ({...todo,formattedDate: new Date(todo.createdAt).toLocaleDateString()}))
})
依赖查询
某些查询可能依赖于其他查询的结果:
// 先获取用户
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:
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 })}}><inputvalue={title}onChange={(e) => setTitle(e.target.value)}/><button type="submit" disabled={mutation.isPending}>{mutation.isPending ? '添加中...' : '添加待办'}</button></form>)
}
乐观更新
乐观更新是提升用户体验的重要技术,通过假设请求会成功,立即更新UI:
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让实现无限滚动变得超级简单:
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>))}<buttononClick={() => fetchNextPage()}disabled={!hasNextPage || isFetchingNextPage}>{isFetchingNextPage? '加载更多...': hasNextPage? '加载更多': '没有更多数据了'}</button></div>)
}
性能优化技巧
React Query已经内置了很多性能优化,但了解这些选项可以让你更好地调整:
缓存时间与失效时间
const queryClient = new QueryClient({defaultOptions: {queries: {// 数据缓存时间(默认5分钟)gcTime: 1000 * 60 * 5,// 数据被认为是新鲜的时间(默认0)staleTime: 1000 * 60,// 重试次数retry: 3,// 窗口重新聚焦时重新获取(默认true)refetchOnWindowFocus: true,},},
})
手动控制重新获取
有时你可能想手动控制数据刷新:
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中:
// 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
}
预取数据
对于重要页面,可以预先获取数据提升用户体验:
// 在路由变化或用户hover某个链接时预取
function prefetchTodoPage(todoId) {queryClient.prefetchQuery({queryKey: ['todo', todoId],queryFn: () => fetchTodoById(todoId)})
}
全局错误处理
设置全局错误处理逻辑:
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很强大,但并非所有场景都适合:
- 客户端状态管理 - 对于纯本地状态,Redux或Context可能更合适
- 极简应用 - 如果应用非常简单,可能不值得引入额外库
- 不需要缓存的API调用 - 某些一次性操作可能不需要React Query
结语
React Query彻底改变了我处理服务器数据的方式。它优雅地解决了React应用中数据获取的绝大多数问题,让我可以专注于业务逻辑而不是重复编写数据获取的样板代码。
我强烈建议任何中大型React应用都考虑使用React Query。它会让你的代码更加简洁、可维护,并提供出色的用户体验。别忘了查阅官方文档,那里有更多高级功能等待探索!
希望这篇入门指南对你有所帮助。编码愉快!