React 数据请求方案
问题
React 中有哪些数据请求方案?SWR、TanStack Query、RTK Query 各有什么特点?如何选择?
React 中有哪些数据请求方案? 从原始到现代分三档:
- 原生方案:
useEffect+fetch/axios,零依赖但坑多——要手动管 loading/error/data、要处理竞态条件、没缓存、没去重、组件卸载后还可能 setState。 - 轻量请求库:SWR(Vercel 出品,约 4KB),主打 stale-while-revalidate 策略。
- 全功能请求库:TanStack Query(约 13KB)、RTK Query(绑定 Redux)、Apollo Client(GraphQL 专用)。
- 核心理念的转变:把服务端状态(异步、可共享、会过期)和客户端状态(UI 状态、表单)分开管理。
SWR、TanStack Query、RTK Query 怎么选?
- SWR:API 极简,一个
useSWR(key, fetcher)就够用。自动帮你处理缓存、去重、聚焦时重新验证、轮询。小到中型项目首选。 - TanStack Query:功能比 SWR 更全——有
useMutation、查询失效精确控制、无限滚动、乐观更新、离线支持、SSR 适配。复杂项目首选,DevTools 也比 SWR 好用。 - RTK Query:如果项目已经在用 Redux Toolkit,直接用它最顺;它会自动生成 hooks 和 reducer,配合 tag 系统做缓存失效。
- Apollo Client:只在用 GraphQL 时考虑,规范化缓存是它的杀手锏。
- 选型口诀:纯前端简单查询用 SWR;复杂业务(增删改查 + 乐观更新 + 缓存协同)用 TanStack Query;已用 Redux 用 RTK Query;GraphQL 用 Apollo。
答案
React 数据请求从最原始的 useEffect + fetch 到现代的请求库,经历了从"命令式"到"声明式"的演进。现代请求库的核心理念是将服务端状态(Server State)与客户端状态(Client State)分离管理。
- 客户端状态:UI 状态、表单输入、模态框开关 → 用
useState、Zustand 等管理 - 服务端状态:来自 API 的数据,具有异步性、共享性、可能过期 → 用 SWR、TanStack Query 等管理
| 方案 | 定位 | 缓存 | 体积 | 学习曲线 |
|---|---|---|---|---|
useEffect + fetch | 原生方案 | 无 | 0 | 低 |
| SWR | 轻量请求库 | stale-while-revalidate | ~4KB | 低 |
| TanStack Query | 全功能请求库 | 多策略 | ~13KB | 中 |
| RTK Query | Redux 生态 | 标签失效 | 含在 RTK 中 | 中高 |
| Apollo Client | GraphQL 客户端 | 规范化缓存 | ~33KB | 高 |
useEffect + fetch 的问题
大多数人入门时都会这样写数据请求:
function useUser(id: string) {
const [data, setData] = useState<User | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false; // 防止组件卸载后 setState
setLoading(true);
fetch(`/api/users/${id}`)
.then(res => res.json())
.then(data => {
if (!cancelled) setData(data);
})
.catch(err => {
if (!cancelled) setError(err);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => { cancelled = true; };
}, [id]);
return { data, error, loading };
}
- 无缓存:切换页面再回来,数据重新加载,用户看到 loading
- 无去重:多个组件请求同一接口,会发送多次请求
- 无自动刷新:数据可能已过期,用户看到的是旧数据
- 竞态条件:快速切换
id时,旧请求可能覆盖新请求的结果(参考接口竞态处理) - 样板代码多:每个请求都要手动管理 loading/error/data 三个状态
- 无乐观更新:修改数据后需要手动刷新列表
这些问题正是 SWR 和 TanStack Query 要解决的。
SWR
SWR 由 Vercel 团队开发,名字来源于 HTTP 缓存策略 stale-while-revalidate —— 先返回缓存(stale),再发起请求验证(revalidate),最后用新数据替换。
核心用法
- npm
- Yarn
- pnpm
- Bun
npm install swr
yarn add swr
pnpm add swr
bun add swr
import useSWR from 'swr';
// fetcher:接收 key 作为参数,返回数据的 Promise
const fetcher = (url: string) => fetch(url).then(res => res.json());
function useUser(id: string) {
const { data, error, isLoading, isValidating, mutate } = useSWR<User>(
`/api/users/${id}`, // key:唯一标识,通常是 URL
fetcher, // fetcher:数据请求函数
);
return { user: data, error, isLoading, isValidating, mutate };
}
function Profile({ id }: { id: string }) {
const { user, error, isLoading } = useUser(id);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>Hello, {user.name}!</div>;
}
data:请求成功返回的数据,未完成时为undefinederror:请求抛出的错误isLoading:首次加载中(无缓存 + 请求中)isValidating:任何请求进行中(包括重新验证)mutate:手动触发数据重新验证或更新缓存
stale-while-revalidate 策略
SWR 的核心工作流程:
这意味着用户几乎总是立即看到内容(来自缓存),同时数据在后台静默更新。
全局配置
通过 SWRConfig 统一配置所有 useSWR 的默认行为:
import { SWRConfig } from 'swr';
const fetcher = async (url: string) => {
const res = await fetch(url);
if (!res.ok) throw new Error('请求失败');
return res.json();
};
function Providers({ children }: { children: React.ReactNode }) {
return (
<SWRConfig value={{
fetcher, // 全局 fetcher,省得每次传
revalidateOnFocus: true, // 窗口聚焦时重新验证
revalidateOnReconnect: true, // 网络恢复时重新验证
dedupingInterval: 2000, // 2 秒内相同 key 去重
errorRetryCount: 3, // 错误重试 3 次
}}>
{children}
</SWRConfig>
);
}
配置好全局 fetcher 后,useSWR 只需要传 key 即可:
// 全局配置了 fetcher 后,可以省略第二个参数
const { data } = useSWR<User>(`/api/users/${id}`);
自动重新验证
SWR 提供多种自动刷新机制,确保用户始终看到最新数据:
const { data } = useSWR('/api/data', fetcher, {
// 窗口聚焦时重新验证(用户切回标签页时)
revalidateOnFocus: true,
// 网络恢复时重新验证(断网重连后)
revalidateOnReconnect: true,
// 轮询间隔(毫秒),0 表示不轮询
refreshInterval: 3000,
// 浏览器不可见时是否继续轮询
refreshWhenHidden: false,
// 断网时是否继续轮询
refreshWhenOffline: false,
});
条件请求
当 key 为 null 或 false 或抛出异常时,SWR 不会发起请求:
// 等 userId 有值后才请求
const { data: user } = useSWR(userId ? `/api/users/${userId}` : null);
// 依赖请求:等第一个请求完成后再请求第二个
const { data: user } = useSWR<User>(`/api/users/${id}`);
const { data: projects } = useSWR<Project[]>(
// user 未加载时返回 null,SWR 不会发起请求
user ? `/api/projects?uid=${user.id}` : null
);
数据突变(Mutation)
修改数据后,需要更新缓存让 UI 同步:
import useSWR, { useSWRMutation } from 'swr';
function TodoList() {
const { data: todos, mutate } = useSWR<Todo[]>('/api/todos');
// ✅ 方式 1:绑定式 mutation(推荐)
const { trigger, isMutating } = useSWRMutation(
'/api/todos',
async (url: string, { arg }: { arg: { title: string } }) => {
return fetch(url, {
method: 'POST',
body: JSON.stringify(arg),
}).then(res => res.json());
}
);
const addTodo = async () => {
// 发起请求并自动更新缓存
await trigger({ title: 'New Todo' });
};
// ✅ 方式 2:乐观更新
const toggleTodo = async (todo: Todo) => {
const updatedTodo = { ...todo, completed: !todo.completed };
// 乐观更新:先更新 UI,再发请求
await mutate(
// 更新函数:修改缓存数据
async (currentTodos) => {
// 发送实际请求
await fetch(`/api/todos/${todo.id}`, {
method: 'PATCH',
body: JSON.stringify({ completed: updatedTodo.completed }),
});
// 返回新数据替换缓存
return currentTodos?.map(t =>
t.id === todo.id ? updatedTodo : t
);
},
{
// 乐观数据:请求完成前先展示这个
optimisticData: todos?.map(t =>
t.id === todo.id ? updatedTodo : t
),
rollbackOnError: true, // 请求失败时回滚
revalidate: false, // 不再重新请求
}
);
};
return (
<ul>
{todos?.map(todo => (
<li key={todo.id} onClick={() => toggleTodo(todo)}>
{todo.completed ? '✅' : '⬜'} {todo.title}
</li>
))}
<button onClick={addTodo} disabled={isMutating}>
{isMutating ? 'Adding...' : 'Add Todo'}
</button>
</ul>
);
}
分页与无限加载
import useSWRInfinite from 'swr/infinite';
interface Page {
data: Item[];
nextCursor: string | null;
}
function useInfiniteList() {
const { data, size, setSize, isValidating } = useSWRInfinite<Page>(
// getKey:根据页码和上一页数据生成当前页的 key
(pageIndex, previousPageData) => {
if (previousPageData && !previousPageData.nextCursor) return null; // 到底了
if (pageIndex === 0) return '/api/items?limit=10';
return `/api/items?cursor=${previousPageData!.nextCursor}&limit=10`;
}
);
// 将多页数据展平为一维数组
const items = data?.flatMap(page => page.data) ?? [];
const isLoadingMore = isValidating && data && data.length === size;
const hasMore = data?.[data.length - 1]?.nextCursor != null;
return {
items,
loadMore: () => setSize(size + 1),
isLoadingMore,
hasMore,
};
}
SWR 架构小结
TanStack Query(React Query)
TanStack Query(原 React Query)是功能最全面的 React 数据请求库,比 SWR 提供更多高级功能。
基础用法
- npm
- Yarn
- pnpm
- Bun
npm install @tanstack/react-query
yarn add @tanstack/react-query
pnpm add @tanstack/react-query
bun add @tanstack/react-query
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// 创建全局 QueryClient 实例
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 数据 1 分钟内视为新鲜,不重新请求
gcTime: 1000 * 60 * 5, // 缓存 5 分钟后垃圾回收(v5 前叫 cacheTime)
retry: 3, // 失败重试 3 次
refetchOnWindowFocus: true, // 窗口聚焦时重新请求
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
</QueryClientProvider>
);
}
import { useQuery } from '@tanstack/react-query';
function useUser(id: string) {
return useQuery({
// queryKey:唯一标识,数组形式,支持序列化的任意值
queryKey: ['users', id],
// queryFn:请求函数
queryFn: async () => {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('请求失败');
return res.json() as Promise<User>;
},
// 可选配置
enabled: !!id, // 条件请求
});
}
Mutation(数据变更)
import { useMutation, useQueryClient } from '@tanstack/react-query';
function useCreateTodo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (newTodo: { title: string }) => {
const res = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
});
return res.json() as Promise<Todo>;
},
// 成功后使 todos 缓存失效,触发重新请求
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
// 乐观更新
onMutate: async (newTodo) => {
// 取消正在进行的 todos 查询(避免覆盖乐观数据)
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 保存当前数据用于回滚
const previousTodos = queryClient.getQueryData(['todos']);
// 乐观更新缓存
queryClient.setQueryData(['todos'], (old: Todo[]) => [
...old,
{ id: Date.now(), ...newTodo, completed: false },
]);
return { previousTodos };
},
// 失败时回滚
onError: (_err, _newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos);
},
});
}
// 使用
function TodoForm() {
const { mutate, isPending } = useCreateTodo();
return (
<button
onClick={() => mutate({ title: 'New Todo' })}
disabled={isPending}
>
{isPending ? 'Creating...' : 'Add Todo'}
</button>
);
}
无限查询
import { useInfiniteQuery } from '@tanstack/react-query';
function useInfinitePosts() {
return useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam }) => {
const res = await fetch(`/api/posts?cursor=${pageParam}&limit=10`);
return res.json() as Promise<{ data: Post[]; nextCursor: string | null }>;
},
initialPageParam: '',
// 告诉 TanStack Query 下一页的参数
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
});
}
function PostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfinitePosts();
// 将多页数据展平
const posts = data?.pages.flatMap(page => page.data) ?? [];
return (
<div>
{posts.map(post => <PostCard key={post.id} post={post} />)}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
}
DevTools
TanStack Query 内置了强大的 DevTools,可以可视化查看所有查询的状态、缓存数据、时间线等:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
{/* 仅在开发环境显示 */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
RTK Query
RTK Query 是 Redux Toolkit 内置的数据请求方案,如果项目已经使用 Redux,它是最自然的选择。
定义 API
RTK Query 采用集中式 API 定义,所有端点在一个地方声明:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
// 定义 API slice
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
// 标签类型:用于缓存失效
tagTypes: ['User', 'Todo'],
endpoints: (builder) => ({
// 查询端点
getUser: builder.query<User, string>({
query: (id) => `/users/${id}`,
providesTags: (result, error, id) => [{ type: 'User', id }],
}),
getTodos: builder.query<Todo[], void>({
query: () => '/todos',
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'Todo' as const, id })),
{ type: 'Todo', id: 'LIST' },
]
: [{ type: 'Todo', id: 'LIST' }],
}),
// 变更端点
addTodo: builder.mutation<Todo, Partial<Todo>>({
query: (body) => ({ url: '/todos', method: 'POST', body }),
// 使 Todo 列表缓存失效 → 自动重新请求
invalidatesTags: [{ type: 'Todo', id: 'LIST' }],
}),
}),
});
// 自动生成的 Hooks
export const {
useGetUserQuery,
useGetTodosQuery,
useAddTodoMutation,
} = apiSlice;
function TodoList() {
const { data: todos, isLoading } = useGetTodosQuery();
const [addTodo, { isLoading: isAdding }] = useAddTodoMutation();
if (isLoading) return <div>Loading...</div>;
return (
<ul>
{todos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
<button onClick={() => addTodo({ title: 'New' })} disabled={isAdding}>
Add
</button>
</ul>
);
}
RTK Query 使用 providesTags 和 invalidatesTags 实现自动缓存失效:
- Query 提供标签(
providesTags):声明这个查询提供了哪些数据 - Mutation 使标签失效(
invalidatesTags):声明这个变更影响了哪些数据 - 当标签失效时,所有提供该标签的查询自动重新请求
这种声明式的缓存失效比手动 invalidateQueries 更容易维护。
方案对比
SWR vs TanStack Query
| 特性 | SWR | TanStack Query |
|---|---|---|
| 体积 | ~4KB | ~13KB |
| 缓存策略 | stale-while-revalidate | stale-while-revalidate + 精细控制 |
| Mutation | useSWRMutation | useMutation(功能更丰富) |
| 无限查询 | useSWRInfinite | useInfiniteQuery(API 更直观) |
| DevTools | 社区版 | 官方内置 |
| 离线支持 | 基础 | 完整的离线模式 |
| SSR/RSC | 支持 | 完整支持(Hydration、Prefetch) |
| 垃圾回收 | 无自动回收 | gcTime 自动回收 |
| Prefetch | preload | prefetchQuery(更多选项) |
| 依赖查询 | 条件 key | enabled 选项 |
| 并行查询 | 多个 Hook | useQueries 批量查询 |
| 选择器 | 无 | select 选项 |
| 取消请求 | 无内置 | AbortSignal 集成 |
| 框架支持 | React | React、Vue、Solid、Svelte、Angular |
选型建议
- SWR:轻量级、API 简洁、Vercel/Next.js 生态首选,适合中小型项目
- TanStack Query:功能最全面、DevTools 好用、社区大,适合中大型项目
- RTK Query:已用 Redux 的项目直接用它,避免引入额外库
- Apollo Client:GraphQL 项目的标配
Server Components 中的数据请求
在 React Server Components 中,数据请求回归到最简单的形式 —— 直接在组件中 await:
// Server Component:直接 async/await,无需任何请求库
async function UserPage({ params }: { params: { id: string } }) {
const user = await fetch(`https://api.example.com/users/${params.id}`, {
// Next.js 扩展:缓存和重新验证
next: { revalidate: 60 }, // 60 秒后重新验证
}).then(res => res.json());
return <UserProfile user={user} />;
}
Server Components 适合静态或不常变化的数据。以下场景仍需客户端请求库:
- 需要实时更新的数据(聊天、通知)
- 用户交互触发的请求(搜索、筛选)
- 乐观更新(点赞、收藏)
- 无限滚动/分页
- 需要离线支持的 PWA
常见面试问题
Q1: SWR 的 stale-while-revalidate 策略是什么?
答案:
stale-while-revalidate 源自 HTTP 缓存头 Cache-Control: max-age=1, stale-while-revalidate=59,核心思想是:
- 先返回缓存(stale):从缓存中立即返回可能过期的数据,用户无需等待
- 后台重新验证(revalidate):同时在后台发起真实请求获取最新数据
- 静默更新:如果新数据与缓存不同,更新缓存并触发 UI 重渲染
这种策略兼顾了即时响应(用户立刻看到内容)和数据新鲜(后台悄悄更新)。
Q2: TanStack Query 的 staleTime 和 gcTime 有什么区别?
答案:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 分钟
gcTime: 1000 * 60 * 5, // 5 分钟(v5 前叫 cacheTime)
},
},
});
| 配置 | 含义 | 默认值 | 影响 |
|---|---|---|---|
staleTime | 数据多久后变"过期" | 0(立即过期) | 过期后再次使用会触发后台 revalidate |
gcTime | 数据多久后从缓存中删除 | 5 分钟 | 删除后再次请求无法使用缓存 |
生命周期示意:
请求完成 ─── staleTime ──→ 变为 stale(过期)
│
├─ 再次访问:先返回缓存,后台 revalidate
│
─── gcTime ──→ 缓存被垃圾回收
│
└─ 再次访问:全新加载(显示 loading)
Q3: 如何实现乐观更新?为什么需要?
答案:
乐观更新是指在服务端确认之前就先更新 UI,让用户操作感觉"即时生效"。典型场景:点赞、收藏、切换开关。
以 TanStack Query 为例:
const likeMutation = useMutation({
mutationFn: (postId: string) => api.likePost(postId),
onMutate: async (postId) => {
// 1. 取消进行中的查询,防止覆盖乐观数据
await queryClient.cancelQueries({ queryKey: ['post', postId] });
// 2. 保存当前数据(用于回滚)
const previous = queryClient.getQueryData(['post', postId]);
// 3. 乐观更新缓存
queryClient.setQueryData(['post', postId], (old: Post) => ({
...old,
liked: true,
likeCount: old.likeCount + 1,
}));
return { previous };
},
onError: (err, postId, context) => {
// 4. 请求失败时回滚
queryClient.setQueryData(['post', postId], context?.previous);
},
onSettled: (data, err, postId) => {
// 5. 无论成功失败,最终都重新验证确保数据一致
queryClient.invalidateQueries({ queryKey: ['post', postId] });
},
});
Q4: 如何处理多个请求的依赖关系?
答案:
SWR 通过条件 key 实现:
// 第一个请求
const { data: user } = useSWR<User>('/api/user');
// 第二个请求:依赖第一个的结果
const { data: projects } = useSWR<Project[]>(
user ? `/api/projects?uid=${user.id}` : null // user 为空时不请求
);
TanStack Query 通过 enabled 选项实现:
const { data: user } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
});
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => fetchProjects(user!.id),
enabled: !!user, // user 存在时才发起请求
});
Q5: SWR 和 TanStack Query 如何处理请求去重?
答案:
两者都会对相同 key 的并发请求自动去重。
SWR 默认在 dedupingInterval(2 秒)内去重:
// 这两个 Hook 在不同组件中,但 key 相同
const { data } = useSWR('/api/users'); // 组件 A
const { data } = useSWR('/api/users'); // 组件 B(2 秒内不会重复请求)
TanStack Query 天然去重 —— 相同 queryKey 的查询共享同一个 Promise:
// 多个组件使用相同 queryKey,只发一次请求
useQuery({ queryKey: ['users'], queryFn: fetchUsers }); // 组件 A
useQuery({ queryKey: ['users'], queryFn: fetchUsers }); // 组件 B(共享同一请求)
Q6: 为什么 TanStack Query 的 queryKey 是数组?
答案:
数组形式的 key 有两个优势:
- 结构化:可以将实体类型和参数分开,便于管理和失效
// 结构化的 key
['todos'] // 所有 todos
['todos', { status: 'done' }] // 筛选条件
['todos', 5] // 单个 todo
// 精确失效
queryClient.invalidateQueries({ queryKey: ['todos'] }); // 使所有 todos 相关缓存失效
queryClient.invalidateQueries({ queryKey: ['todos', 5] }); // 只失效 id=5 的
- 自动序列化比较:对象参数无论属性顺序如何,都能正确匹配
// 以下两个 key 视为相同(对象属性顺序不影响)
['todos', { status: 'done', page: 1 }]
['todos', { page: 1, status: 'done' }]
Q7: 如何在 Next.js App Router 中结合 Server Components 和 TanStack Query?
答案:
在 Server Component 中预取数据,传递给 Client Component 作为初始数据,实现首屏 SSR + 后续客户端缓存:
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
import { TodoList } from './TodoList';
// Server Component
export default async function TodosPage() {
const queryClient = new QueryClient();
// 在服务端预取数据
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: () => fetch('https://api.example.com/todos').then(r => r.json()),
});
return (
// 将服务端缓存脱水后传给客户端
<HydrationBoundary state={dehydrate(queryClient)}>
<TodoList /> {/* Client Component,使用 useQuery 读取已预取的数据 */}
</HydrationBoundary>
);
}
'use client';
import { useQuery } from '@tanstack/react-query';
export function TodoList() {
// 初次渲染:直接使用服务端预取的数据(无 loading)
// 后续交互:自动 revalidate,保持数据新鲜
const { data: todos } = useQuery<Todo[]>({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
});
return (
<ul>
{todos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
</ul>
);
}
Q8: 什么时候不需要请求库,直接 useEffect + fetch 就够了?
答案:
以下场景原生方案就足够:
- 一次性请求:组件挂载请求一次,不需要缓存和刷新
- 极简项目:页面少、不需要跨组件共享数据
- 不依赖缓存:每次都要最新数据,且不介意 loading
- 学习/原型阶段:快速验证想法,不需要完善的错误处理
但即使是简单项目,使用 SWR(~4KB)带来的缓存、去重、自动刷新也几乎没有成本。如果项目有超过 3 个接口,建议使用请求库。
Q9: 如何封装一个通用的请求 Hook?
答案:
基于 SWR 封装一个带类型安全和错误处理的通用 Hook:
import useSWR, { SWRConfiguration } from 'swr';
// 统一的 API 响应格式
interface ApiResponse<T> {
code: number;
data: T;
message: string;
}
// 通用 fetcher:处理 HTTP 错误和业务错误
async function apiFetcher<T>(url: string): Promise<T> {
const res = await fetch(url, {
headers: { Authorization: `Bearer ${getToken()}` },
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const json: ApiResponse<T> = await res.json();
if (json.code !== 0) {
throw new Error(json.message || '请求失败');
}
return json.data;
}
// 通用请求 Hook
export function useRequest<T>(
key: string | null,
options?: SWRConfiguration<T>
) {
return useSWR<T>(key, apiFetcher, {
// 错误重试时使用指数退避
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
if (retryCount >= 3) return; // 最多重试 3 次
if (error.message.includes('401')) return; // 401 不重试
setTimeout(() => revalidate({ retryCount }), 2 ** retryCount * 1000);
},
...options,
});
}
// 使用
const { data, error } = useRequest<User>(`/api/users/${id}`);
Q10: SWR 和 TanStack Query 对比 Redux 管理异步数据,有什么优势?
答案:
传统 Redux 管理异步数据需要大量样板代码:
// ❌ Redux 传统方式:需要定义 action、reducer、thunk
// 1. 定义 action types
const FETCH_USERS_REQUEST = 'FETCH_USERS_REQUEST';
const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS';
const FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE';
// 2. 定义 reducer(管理 loading/data/error)
// 3. 定义 thunk(发起请求)
// 4. 在组件中 dispatch + useSelector
// 5. 手动管理缓存失效、重试、去重...
// ✅ TanStack Query:一行代码搞定
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
| 维度 | Redux(手动管理) | SWR / TanStack Query |
|---|---|---|
| 样板代码 | 大量 action/reducer | 几乎零配置 |
| 缓存管理 | 手动实现 | 自动 |
| 请求去重 | 手动实现 | 自动 |
| 自动刷新 | 手动实现 | 内置 |
| 乐观更新 | 手动实现 | 内置 API |
| 垃圾回收 | 手动实现 | 自动 |
| DevTools | Redux DevTools | 专属 DevTools |
核心优势是将服务端状态从全局 store 中剥离出来,让 Redux 专注于管理真正的客户端状态,各司其职。