04-性能优化与最佳实践——12. 请求缓存 - React Query / SWR
2026/6/25 12:26:12 网站建设 项目流程

12. 请求缓存 - React Query / SWR

概述

React Query 和 SWR 是专为 React 设计的服务端状态管理库,它们自动处理数据获取、缓存、重新验证、后台更新等复杂逻辑,让开发者专注于业务而非请求状态管理。

维度内容
What管理服务端状态的库,自动处理缓存、重试、后台更新
Why减少重复请求,自动处理加载/错误状态,提升用户体验
When需要缓存、重试、后台更新的数据获取场景
Where数据获取组件中,替代手动 fetch/axios + useState/useEffect
Who需要优化数据获取的开发者
Howconst { data } = useQuery(['key'], fetcher)useSWR(key, fetcher)

1. 为什么需要请求缓存库

1.1 传统数据获取的问题

// ❌ 传统方式:手动管理所有状态 function TodoList() { const [todos, setTodos] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetchTodos(); }, []); async function fetchTodos() { try { setLoading(true); const response = await fetch('/api/todos'); const data = await response.json(); setTodos(data); } catch (err) { setError(err); } finally { setLoading(false); } } // 还需要处理: // - 缓存 // - 重新验证 // - 后台更新 // - 并发请求去重 // - 分页/无限加载 // - 乐观更新 // ... 代码会越来越复杂 }

1.2 React Query / SWR 解决方案

// ✅ 使用 React Query import { useQuery } from '@tanstack/react-query'; function TodoList() { const { data: todos, isLoading, error } = useQuery({ queryKey: ['todos'], queryFn: () => fetch('/api/todos').then(res => res.json()) }); // 自动处理: // ✅ 加载和错误状态 // ✅ 缓存 // ✅ 后台重新验证 // ✅ 请求去重 // ✅ 窗口焦点重新获取 // ✅ 分页/无限加载 // ✅ 乐观更新 if (isLoading) return <div>加载中...</div>; if (error) return <div>错误: {error.message}</div>; return <TodoItems todos={todos} />; }

2. React Query (TanStack Query) 详解

2.1 安装与配置

npminstall@tanstack/react-query
// main.jsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5, // 5分钟内数据被认为是新鲜的 gcTime: 1000 * 60 * 10, // 缓存保留10分钟 retry: 3, // 失败重试3次 retryDelay: 1000, // 重试延迟1秒 refetchOnWindowFocus: true, // 窗口焦点时重新获取 refetchOnReconnect: true, // 网络重连时重新获取 }, }, }); function App() { return ( <QueryClientProvider client={queryClient}> <YourApp /> <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ); }

2.2 基础 useQuery

import { useQuery } from '@tanstack/react-query'; // 获取用户列表 function Users() { const { data, isLoading, error, isError, refetch, // 手动重新获取 dataUpdatedAt // 最后更新时间 } = useQuery({ queryKey: ['users'], queryFn: async () => { const response = await fetch('https://jsonplaceholder.typicode.com/users'); if (!response.ok) throw new Error('获取失败'); return response.json(); }, }); if (isLoading) return <Spinner />; if (isError) return <Error message={error.message} />; return ( <div> <button onClick={() => refetch()}>刷新</button> <ul> {data.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> ); }

2.3 带参数的查询

function UserDetail({ userId }) { const { data: user, isLoading } = useQuery({ queryKey: ['user', userId], queryFn: async () => { const response = await fetch(`/api/users/${userId}`); return response.json(); }, enabled: !!userId, // userId 存在时才执行 }); if (!userId) return <div>请选择用户</div>; if (isLoading) return <Spinner />; return <div>{user?.name}</div>; }

2.4 依赖查询

function DependentQueries({ userId }) { // 第一个查询:获取用户信息 const { data: user } = useQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), enabled: !!userId, }); // 第二个查询:依赖第一个查询的结果 const { data: posts } = useQuery({ queryKey: ['user-posts', user?.id], queryFn: () => fetchUserPosts(user.id), enabled: !!user?.id, // 等待 user 加载完成 }); return <div>{/* 渲染内容 */}</div>; }

2.5 并行查询

function ParallelQueries() { // 方式1:多个 useQuery 调用 const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers }); const postsQuery = useQuery({ queryKey: ['posts'], queryFn: fetchPosts }); const commentsQuery = useQuery({ queryKey: ['comments'], queryFn: fetchComments }); if (usersQuery.isLoading || postsQuery.isLoading || commentsQuery.isLoading) { return <Spinner />; } return <div>{/* 渲染所有数据 */}</div>; } // 方式2:useQueries(动态数量的查询) function DynamicParallelQueries({ ids }) { const userQueries = useQueries({ queries: ids.map(id => ({ queryKey: ['user', id], queryFn: () => fetchUser(id), })), }); const isLoading = userQueries.some(query => query.isLoading); if (isLoading) return <Spinner />; return ( <div> {userQueries.map(({ data }, index) => ( <div key={index}>{data?.name}</div> ))} </div> ); }

2.6 分页查询

import { useQuery } from '@tanstack/react-query'; function PaginatedList() { const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(0); const { data, isLoading, isFetching } = useQuery({ queryKey: ['items', page], queryFn: async () => { const response = await fetch(`/api/items?page=${page}&limit=10`); const result = await response.json(); setTotalPages(result.totalPages); return result.data; }, keepPreviousData: true, // 保留旧数据,避免加载状态闪烁 }); return ( <div> {isLoading ? ( <Spinner /> ) : ( <> <ItemList items={data} /> {isFetching && <div>加载中...</div>} <Pagination page={page} totalPages={totalPages} onPageChange={setPage} /> </> )} </div> ); }

2.7 无限加载

import { useInfiniteQuery } from '@tanstack/react-query'; function InfiniteScroll() { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, } = useInfiniteQuery({ queryKey: ['items'], queryFn: async ({ pageParam = 1 }) => { const response = await fetch(`/api/items?page=${pageParam}&limit=20`); return response.json(); }, getNextPageParam: (lastPage, pages) => { // 返回下一页的页码,如果没有下一页返回 undefined return lastPage.hasNext ? lastPage.page + 1 : undefined; }, initialPageParam: 1, }); if (isLoading) return <Spinner />; return ( <div> {data.pages.map((page, i) => ( <div key={i}> {page.items.map(item => ( <div key={item.id}>{item.name}</div> ))} </div> ))} {hasNextPage && ( <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}> {isFetchingNextPage ? '加载更多...' : '加载更多'} </button> )} </div> ); }

2.8 数据变更 - useMutation

import { useMutation, useQueryClient } from '@tanstack/react-query'; function CreatePost() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (newPost) => { return fetch('/api/posts', { method: 'POST', body: JSON.stringify(newPost), headers: { 'Content-Type': 'application/json' }, }).then(res => res.json()); }, onSuccess: (data) => { // 成功后的操作 console.log('创建成功:', data); // 方式1:使缓存失效,重新获取 queryClient.invalidateQueries({ queryKey: ['posts'] }); // 方式2:直接更新缓存 queryClient.setQueryData(['posts'], (old) => [...old, data]); }, onError: (error) => { console.error('创建失败:', error); }, }); return ( <button onClick={() => mutation.mutate({ title: '新文章' })}> {mutation.isPending ? '创建中...' : '创建文章'} </button> ); }

2.9 乐观更新

function OptimisticUpdate() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (newTodo) => { return fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo), }).then(res => res.json()); }, onMutate: async (newTodo) => { // 取消进行中的查询 await queryClient.cancelQueries({ queryKey: ['todos'] }); // 保存当前状态 const previousTodos = queryClient.getQueryData(['todos']); // 乐观更新 queryClient.setQueryData(['todos'], (old) => [...old, newTodo]); // 返回上下文,用于回滚 return { previousTodos }; }, onError: (err, newTodo, context) => { // 发生错误时回滚 queryClient.setQueryData(['todos'], context.previousTodos); }, onSettled: () => { // 无论成功或失败,重新获取数据确保同步 queryClient.invalidateQueries({ queryKey: ['todos'] }); }, }); return <button onClick={() => mutation.mutate({ title: '新任务' })}> 添加任务 </button>; }

3. SWR 详解

3.1 安装与配置

npminstallswr
// 全局配置 import { SWRConfig } from 'swr'; function App() { return ( <SWRConfig value={{ fetcher: (url) => fetch(url).then(res => res.json()), revalidateOnFocus: true, revalidateOnReconnect: true, refreshInterval: 0, errorRetryCount: 3, }} > <YourApp /> </SWRConfig> ); }

3.2 基础 useSWR

import useSWR from 'swr'; // 全局 fetcher const fetcher = (url) => fetch(url).then(res => res.json()); function Users() { const { data, error, isLoading, isValidating, mutate } = useSWR( 'https://jsonplaceholder.typicode.com/users', fetcher ); if (isLoading) return <Spinner />; if (error) return <Error message={error.message} />; return ( <div> <button onClick={() => mutate()}>刷新</button> <ul> {data.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> ); }

3.3 带参数的请求

function UserDetail({ userId }) { const { data, isLoading } = useSWR( userId ? `/api/users/${userId}` : null, // null 时不请求 fetcher ); if (!userId) return <div>请选择用户</div>; if (isLoading) return <Spinner />; return <div>{data?.name}</div>; }

3.4 依赖请求

function DependentSWR({ userId }) { // 获取用户信息 const { data: user } = useSWR( userId ? `/api/users/${userId}` : null, fetcher ); // 依赖用户信息获取文章 const { data: posts } = useSWR( user ? `/api/users/${user.id}/posts` : null, fetcher ); return <div>{/* 渲染 */}</div>; }

3.5 分页

function PaginatedSWR() { const [page, setPage] = useState(1); const { data, isLoading } = useSWR( `/api/items?page=${page}&limit=10`, fetcher, { keepPreviousData: true, // 保留旧数据 } ); return ( <div> <ItemList items={data?.items} /> <Pagination page={page} onPageChange={setPage} /> </div> ); }

3.6 无限加载

import useSWRInfinite from 'swr/infinite'; function InfiniteSWR() { const getKey = (pageIndex, previousPageData) => { // 到达最后一页 if (previousPageData && !previousPageData.hasNext) return null; // 第一页 page=1 return `/api/items?page=${pageIndex + 1}&limit=20`; }; const { data, size, setSize, isLoading } = useSWRInfinite(getKey, fetcher); const items = data ? data.flatMap(page => page.items) : []; const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined'); const isEmpty = data?.[0]?.items?.length === 0; const isReachingEnd = isEmpty || (data && data[data.length - 1]?.items?.length < 20); return ( <div> {items.map(item => ( <div key={item.id}>{item.name}</div> ))} {!isReachingEnd && ( <button onClick={() => setSize(size + 1)} disabled={isLoadingMore}> {isLoadingMore ? '加载中...' : '加载更多'} </button> )} </div> ); }

3.7 数据变更

import useSWR, { mutate } from 'swr'; function CreatePostSWR() { const { data: posts, mutate: mutatePosts } = useSWR('/api/posts', fetcher); const createPost = async (newPost) => { // 乐观更新 const optimisticPosts = [...posts, newPost]; mutatePosts(optimisticPosts, false); // 不重新验证 try { const response = await fetch('/api/posts', { method: 'POST', body: JSON.stringify(newPost), }); const data = await response.json(); // 使用服务器返回的数据更新 mutatePosts([...posts, data]); } catch (error) { // 发生错误时回滚 mutatePosts(posts); console.error(error); } }; return <button onClick={() => createPost({ title: '新文章' })}> 创建文章 </button>; } // 全局 mutate function UpdateUser() { const updateUser = async (userId, userData) => { // 更新特定 key 的数据 mutate(`/api/users/${userId}`, async () => { const response = await fetch(`/api/users/${userId}`, { method: 'PUT', body: JSON.stringify(userData), }); return response.json(); }); }; return <button onClick={() => updateUser(1, { name: 'New Name' })}> 更新用户 </button>; }

4. React Query vs SWR 对比

4.1 特性对比

特性React QuerySWR
学习曲线中等平缓
API 设计功能丰富,选项多简洁,易上手
DevTools优秀的开发者工具基础版
TypeScript极好极好
体积~13KB~5KB
无限查询useInfiniteQueryuseSWRInfinite
并行查询useQueries多个 useSWR
依赖查询enabled 选项key 为 null
乐观更新内置支持需要手动实现
GC 时间可配置 gcTime无内置 GC
React 19 支持完全支持完全支持

4.2 选择建议

// 选择 React Query 如果: // - 需要复杂的数据同步逻辑 // - 需要强大的开发者工具 // - 需要细粒度的缓存控制 // - 项目已经使用 TanStack 生态 // 选择 SWR 如果: // - 追求简洁和轻量 // - 只需要基础的缓存和重新验证 // - 项目使用 Next.js(SWR 由 Vercel 开发) // - 对包体积有严格要求

5. 高级模式

5.1 预取数据

// React Query const queryClient = useQueryClient(); // 鼠标悬停时预取 function UserLink({ userId }) { const prefetchUser = () => { queryClient.prefetchQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), }); }; return ( <Link to={`/users/${userId}`} onMouseEnter={prefetchUser} > 查看用户 </Link> ); }

5.2 SSR/SSG 数据获取

// Next.js + SWR function Profile({ fallbackData }) { const { data } = useSWR('/api/user', fetcher, { fallbackData }); return <div>{data.name}</div>; } // React Query + Next.js import { dehydrate, HydrationBoundary } from '@tanstack/react-query'; export async function getServerSideProps() { const queryClient = new QueryClient(); await queryClient.prefetchQuery({ queryKey: ['user'], queryFn: fetchUser, }); return { props: { dehydratedState: dehydrate(queryClient), }, }; }

5.3 轮询 / 实时数据

// React Query 轮询 const { data } = useQuery({ queryKey: ['realtime-data'], queryFn: fetchData, refetchInterval: 5000, // 每5秒重新获取 refetchIntervalInBackground: true, // 后台也轮询 }); // SWR 轮询 const { data } = useSWR('/api/realtime', fetcher, { refreshInterval: 5000, refreshWhenHidden: true, });

6. 性能优化技巧

6.1 缓存策略

// React Query 缓存策略 const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, // 5分钟内不重新获取 gcTime: 10 * 60 * 1000, // 10分钟后垃圾回收 refetchOnWindowFocus: false, // 不自动重新获取 }, }, }); // 针对特定查询 const { data } = useQuery({ queryKey: ['static-data'], queryFn: fetchStaticData, staleTime: Infinity, // 永不过期 gcTime: Infinity, // 永不清理 });

7. 完整示例:任务管理应用

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; // API 函数 const api = { fetchTasks: () => fetch('/api/tasks').then(res => res.json()), createTask: (task) => fetch('/api/tasks', { method: 'POST', body: JSON.stringify(task), headers: { 'Content-Type': 'application/json' }, }).then(res => res.json()), updateTask: ({ id, updates }) => fetch(`/api/tasks/${id}`, { method: 'PATCH', body: JSON.stringify(updates), headers: { 'Content-Type': 'application/json' }, }).then(res => res.json()), deleteTask: (id) => fetch(`/api/tasks/${id}`, { method: 'DELETE' }), }; function TaskManager() { const [filter, setFilter] = useState('all'); const queryClient = useQueryClient(); // 获取任务列表 const { data: tasks = [], isLoading } = useQuery({ queryKey: ['tasks'], queryFn: api.fetchTasks, }); // 创建任务 const createMutation = useMutation({ mutationFn: api.createTask, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['tasks'] }); }, }); // 更新任务 const updateMutation = useMutation({ mutationFn: api.updateTask, onMutate: async ({ id, updates }) => { await queryClient.cancelQueries({ queryKey: ['tasks'] }); const previousTasks = queryClient.getQueryData(['tasks']); queryClient.setQueryData(['tasks'], (old) => old.map(task => task.id === id ? { ...task, ...updates } : task) ); return { previousTasks }; }, onError: (err, variables, context) => { queryClient.setQueryData(['tasks'], context.previousTasks); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ['tasks'] }); }, }); // 删除任务 const deleteMutation = useMutation({ mutationFn: api.deleteTask, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['tasks'] }); }, }); const filteredTasks = tasks.filter(task => { if (filter === 'active') return !task.completed; if (filter === 'completed') return task.completed; return true; }); const handleAddTask = (title) => { createMutation.mutate({ title, completed: false }); }; const handleToggleTask = (id, completed) => { updateMutation.mutate({ id, updates: { completed: !completed } }); }; const handleDeleteTask = (id) => { deleteMutation.mutate(id); }; if (isLoading) return <div>加载中...</div>; return ( <div> <h1>任务管理</h1> <AddTaskForm onAdd={handleAddTask} /> <div> <button onClick={() => setFilter('all')}>全部</button> <button onClick={() => setFilter('active')}>未完成</button> <button onClick={() => setFilter('completed')}>已完成</button> </div> <TaskList tasks={filteredTasks} onToggle={handleToggleTask} onDelete={handleDeleteTask} /> {createMutation.isPending && <div>添加中...</div>} </div> ); } function AddTaskForm({ onAdd }) { const [title, setTitle] = useState(''); const handleSubmit = (e) => { e.preventDefault(); if (title.trim()) { onAdd(title); setTitle(''); } }; return ( <form onSubmit={handleSubmit}> <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="添加新任务..." /> <button type="submit">添加</button> </form> ); } function TaskList({ tasks, onToggle, onDelete }) { return ( <ul> {tasks.map(task => ( <li key={task.id}> <input type="checkbox" checked={task.completed} onChange={() => onToggle(task.id, task.completed)} /> <span style={{ textDecoration: task.completed ? 'line-through' : 'none' }}> {task.title} </span> <button onClick={() => onDelete(task.id)}>删除</button> </li> ))} </ul> ); } export default TaskManager;

8. 总结

核心要点

要点说明
核心价值自动管理服务端状态,减少样板代码
主要特性缓存、重试、后台更新、乐观更新
React Query功能全面,适合复杂场景
SWR轻量简洁,适合中小项目

记忆口诀

服务状态管理难,React Query SWR 来帮忙
缓存重试自动做,加载错误不用忙
乐观更新体验好,后台刷新数据强


9. 相关资源

  • React Query 官方文档
  • SWR 官方文档
  • React Query DevTools

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询