三年前接手一个电商后台项目时,我还在手动维护loading状态、错误处理和缓存逻辑。一个简单的商品列表页就充斥着useState、useEffect和一堆条件判断。直到偶然尝试了TanStack Query(原React Query),才意识到数据请求管理可以如此优雅。现在,我已经在团队所有新项目中强制推行这套方案。
这个库本质上解决的是"服务器状态管理"问题。与Redux等客户端状态管理工具不同,它专门处理异步数据流,提供了开箱即用的缓存策略、后台刷新、请求去重等能力。根据官方基准测试,使用后平均减少约40%与数据请求相关的代码量。
javascript复制const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
staleTime: 5 * 60 * 1000 // 5分钟缓存
})
通过staleTime和cacheTime的精细控制,可以实现:
关键技巧:对金融类应用设置较短的
staleTime(如30秒),对内容型应用可延长至10分钟
传统模式需要手动处理的场景:
javascript复制// 旧方案
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
fetchData()
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [])
TanStack Query方案:
javascript复制// 新方案
const { data, isLoading, error } = useQuery(['key'], fetchData)
自动处理以下场景:
javascript复制// 用户信息查询
const { data: user } = useQuery(['user', userId], getUser)
// 根据用户权限获取菜单
const { data: menu } = useQuery(
['menu', user?.role],
getMenuByRole,
{ enabled: !!user } // 依赖user存在才执行
)
这种声明式依赖比useEffect的依赖数组更直观,且能避免竞态条件。
javascript复制const [page, setPage] = useState(1)
const { data, isPreviousData } = useQuery({
queryKey: ['projects', page],
queryFn: () => fetchProjects(page),
keepPreviousData: true // 保持上一页数据直到新数据加载完成
})
这个配置可以实现:
javascript复制const queryClient = useQueryClient()
const mutation = useMutation(updateUser, {
onSuccess: () => {
queryClient.invalidateQueries(['user']) // 提交成功后立即刷新用户数据
toast.success('更新成功')
}
})
相比手动处理:
javascript复制// 并行查询
const [user, permissions] = useQueries({
queries: [
{ queryKey: ['user'], queryFn: fetchUser },
{ queryKey: ['permissions'], queryFn: fetchPermissions }
]
})
// 依赖查询
const projects = useQuery({
queryKey: ['projects', user.data?.teamId],
queryFn: () => fetchProjects(user.data.teamId),
enabled: !!user.data
})
javascript复制new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false, // 禁用窗口聚焦刷新
retry: 1 // 失败后只重试1次
}
}
})
javascript复制// 细粒度缓存
const { data: orders } = useQuery(
['orders', { status: 'pending', page: 1 }],
fetchOrders
)
// 批量失效
queryClient.invalidateQueries({
queryKey: ['orders'],
exact: false // 模糊匹配所有orders相关查询
})
javascript复制// Next.js示例
export async function getServerSideProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery(['posts'], fetchPosts)
return { props: { dehydratedState: dehydrate(queryClient) } }
}
function Page({ dehydratedState }) {
const queryClient = useQueryClient()
return (
<Hydrate state={dehydratedState}>
{/* 页面内容 */}
</Hydrate>
)
}
typescript复制import { UseQueryResult } from '@tanstack/react-query'
type User = { id: string; name: string }
const useUser = (id: string): UseQueryResult<User> =>
useQuery(['user', id], () => fetchUser(id))
javascript复制// 测试配置
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false // 测试时禁用重试
}
}
})
// 组件测试
test('loading state', async () => {
render(
<QueryClientProvider client={queryClient}>
<UserProfile />
</QueryClientProvider>
)
expect(screen.getByTestId('spinner')).toBeInTheDocument()
})
javascript复制const { data, error } = useQuery(['key'], fetcher, {
useErrorBoundary: (error) => error instanceof CriticalError
})
// 配合React Error Boundary使用
class ErrorBoundary extends React.Component {
state = { hasError: false }
static getDerivedStateFromError() {
return { hasError: true }
}
componentDidCatch(error) {
if (error instanceof CriticalError) {
reportToSentry(error)
}
}
}
对于已有项目,建议分阶段迁移:
在重构过程中特别注意:
useEffect中的数据请求useMutation替代直接的事件处理函数从个人经验看,一个中等复杂度的页面(约15个请求)迁移后通常能减少30%-50%的状态管理代码。在最近的后台系统重构中,我们成功将平均请求相关代码量从120行/页面降至65行,同时错误处理覆盖率从72%提升到98%。