1. React useQuery 使用指南
作为一名长期使用React的前端开发者,我深刻理解数据获取在应用开发中的重要性。useQuery作为React Query库的核心hook,彻底改变了我们处理数据获取和状态管理的方式。它不仅仅是一个简单的数据获取工具,更是一套完整的数据同步解决方案。
2. useQuery核心概念解析
2.1 什么是useQuery
useQuery是React Query库提供的一个自定义Hook,专门用于在React组件中获取、缓存和更新服务器状态。与传统的useEffect+fetch方案相比,它提供了开箱即用的缓存、后台刷新、错误重试等强大功能。
javascript复制import { useQuery } from 'react-query'
function UserProfile({ userId }) {
const { data, error, isLoading } = useQuery(['user', userId], () =>
fetch(`/api/users/${userId}`).then(res => res.json())
)
// 组件渲染逻辑...
}
2.2 为什么选择useQuery
在传统React应用中,我们通常面临以下痛点:
- 需要手动管理加载状态
- 缺乏内置缓存机制
- 错误处理重复代码多
- 数据过期后需要手动刷新
useQuery通过以下特性解决了这些问题:
- 自动缓存和后台刷新
- 请求去重和自动重试
- 窗口焦点重新获取数据
- 分页和无限加载支持
3. useQuery深度使用指南
3.1 基本配置参数
useQuery接受两个主要参数:
- 查询键(queryKey):用于标识和缓存查询结果的唯一键
- 查询函数(queryFn):返回Promise的数据获取函数
javascript复制const { data } = useQuery(
['todos', { status: 'done' }], // 查询键
() => fetch('/todos?status=done').then(res => res.json()) // 查询函数
)
3.2 高级配置选项
useQuery还支持大量配置选项来定制其行为:
javascript复制const { data } = useQuery(['todos'], fetchTodos, {
staleTime: 5 * 60 * 1000, // 5分钟内数据不会重新获取
cacheTime: 30 * 60 * 1000, // 30分钟后清除缓存
retry: 3, // 失败时自动重试3次
retryDelay: attempt => Math.min(attempt * 1000, 30000), // 重试延迟
refetchOnWindowFocus: true, // 窗口获得焦点时重新获取
enabled: !!userId, // 有条件地启用查询
})
4. 实际应用场景
4.1 分页查询实现
useQuery非常适合实现分页查询:
javascript复制function Todos({ page }) {
const { data } = useQuery(['todos', page], () =>
fetch(`/todos?page=${page}`).then(res => res.json())
)
return (
<div>
{data?.items.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
)
}
4.2 依赖查询
当查询需要依赖其他查询结果时:
javascript复制function UserPosts({ userId }) {
// 先获取用户信息
const { data: user } = useQuery(['user', userId], getUser)
// 然后获取用户帖子(依赖用户信息)
const { data: posts } = useQuery(
['posts', userId],
() => getPostsByUser(user.id),
{
// 只有用户信息加载完成后才执行此查询
enabled: !!user,
}
)
// 渲染逻辑...
}
5. 性能优化技巧
5.1 选择性重取
通过配置staleTime和refetchInterval来优化性能:
javascript复制useQuery(['market-data'], fetchMarketData, {
staleTime: 1000 * 60 * 5, // 5分钟内数据视为新鲜
refetchInterval: 1000 * 60 * 10, // 每10分钟后台刷新
refetchIntervalInBackground: true // 即使应用在后台也刷新
})
5.2 查询取消
对于长时间运行的查询,可以实现取消功能:
javascript复制const query = useQuery(['todos'], ({ signal }) =>
fetch('/todos', { signal }).then(res => res.json())
)
// 需要时可以取消查询
query.refetch.abort()
6. 常见问题解决方案
6.1 处理加载和错误状态
javascript复制function UserProfile() {
const { data, error, isLoading, isError } = useQuery(['user'], fetchUser)
if (isLoading) return <Spinner />
if (isError) return <Error message={error.message} />
return <Profile user={data} />
}
6.2 手动刷新数据
javascript复制function DataDisplay() {
const { data, refetch } = useQuery(['data'], fetchData)
return (
<div>
<button onClick={() => refetch()}>刷新数据</button>
{/* 显示数据 */}
</div>
)
}
7. 与状态管理库集成
7.1 与Redux共存
useQuery可以替代Redux中大部分数据获取逻辑:
javascript复制// 传统Redux方式
dispatch(fetchUserStart())
try {
const user = await fetchUser(userId)
dispatch(fetchUserSuccess(user))
} catch (error) {
dispatch(fetchUserFailure(error))
}
// useQuery方式 - 一行代码替代上述所有逻辑
const { data: user } = useQuery(['user', userId], () => fetchUser(userId))
7.2 与Context API结合
javascript复制const UserContext = createContext()
function UserProvider({ children }) {
const { data: user } = useQuery(['currentUser'], fetchCurrentUser)
return (
<UserContext.Provider value={user}>
{children}
</UserContext.Provider>
)
}
8. 测试策略
8.1 单元测试组件
使用@testing-library/react-hooks测试useQuery:
javascript复制import { renderHook } from '@testing-library/react-hooks'
import { useQuery } from 'react-query'
test('should fetch user data', async () => {
const mockUser = { id: 1, name: 'John' }
// 模拟API调用
const fetchUser = jest.fn(() => Promise.resolve(mockUser))
const { result, waitFor } = renderHook(() =>
useQuery(['user', 1], fetchUser)
)
await waitFor(() => result.current.isSuccess)
expect(fetchUser).toHaveBeenCalledTimes(1)
expect(result.current.data).toEqual(mockUser)
})
8.2 模拟服务器响应
使用Mock Service Worker进行API模拟:
javascript复制import { setupWorker, rest } from 'msw'
const worker = setupWorker(
rest.get('/api/user', (req, res, ctx) => {
return res(
ctx.json({ id: 1, name: 'Mock User' })
)
})
)
// 在测试前启动mock server
beforeAll(() => worker.start())
9. 高级模式
9.1 无限滚动列表
javascript复制import { useInfiniteQuery } from 'react-query'
function Projects() {
const {
data,
fetchNextPage,
hasNextPage,
} = useInfiniteQuery(
'projects',
({ pageParam = 0 }) => fetchProjects(pageParam),
{
getNextPageParam: (lastPage) => lastPage.nextPage,
}
)
return (
<div>
{data.pages.map((page, i) => (
<Fragment key={i}>
{page.projects.map(project => (
<Project key={project.id} project={project} />
))}
</Fragment>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage}
>
加载更多
</button>
</div>
)
}
9.2 乐观更新
javascript复制const queryClient = useQueryClient()
useMutation(updateTodo, {
onMutate: async newTodo => {
// 取消当前所有待处理的查询
await queryClient.cancelQueries(['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(['todos'])
}
})
10. 实际项目经验分享
在大型电商项目中,我们使用useQuery管理了数百个数据端点。以下是一些关键经验:
-
查询键设计:建立统一的命名规范,如
['resource', id]或['collection', { filters }] -
全局配置:通过QueryClientProvider设置默认选项
javascript复制const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
retry: 2,
},
},
})
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* 应用内容 */}
</QueryClientProvider>
)
}
- 性能监控:使用React Query Devtools分析查询性能
javascript复制import { ReactQueryDevtools } from 'react-query/devtools'
function App() {
return (
<>
{/* 应用内容 */}
<ReactQueryDevtools initialIsOpen={false} />
</>
)
}
- 错误处理:创建统一的错误处理层
javascript复制const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error) => {
// 统一的错误处理逻辑
notify(error.message)
},
}),
})
- 缓存管理:合理设置cacheTime,避免内存泄漏
javascript复制useQuery(['heavy-data'], fetchHeavyData, {
cacheTime: 0 // 对于特别大的数据,可以禁用缓存
})
