跳到主要内容

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)分离管理。

服务端状态 vs 客户端状态
  • 客户端状态:UI 状态、表单输入、模态框开关 → 用 useStateZustand 等管理
  • 服务端状态:来自 API 的数据,具有异步性、共享性、可能过期 → 用 SWR、TanStack Query 等管理
方案定位缓存体积学习曲线
useEffect + fetch原生方案0
SWR轻量请求库stale-while-revalidate~4KB
TanStack Query全功能请求库多策略~13KB
RTK QueryRedux 生态标签失效含在 RTK 中中高
Apollo ClientGraphQL 客户端规范化缓存~33KB

useEffect + fetch 的问题

大多数人入门时都会这样写数据请求:

hooks/useUser.ts
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 };
}
原生方案存在的问题
  1. 无缓存:切换页面再回来,数据重新加载,用户看到 loading
  2. 无去重:多个组件请求同一接口,会发送多次请求
  3. 无自动刷新:数据可能已过期,用户看到的是旧数据
  4. 竞态条件:快速切换 id 时,旧请求可能覆盖新请求的结果(参考接口竞态处理
  5. 样板代码多:每个请求都要手动管理 loading/error/data 三个状态
  6. 无乐观更新:修改数据后需要手动刷新列表

这些问题正是 SWR 和 TanStack Query 要解决的。

SWR

SWR 由 Vercel 团队开发,名字来源于 HTTP 缓存策略 stale-while-revalidate —— 先返回缓存(stale),再发起请求验证(revalidate),最后用新数据替换

核心用法

npm install swr
hooks/useUser.ts
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 };
}
components/Profile.tsx
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>;
}
SWR 返回值说明
  • data:请求成功返回的数据,未完成时为 undefined
  • error:请求抛出的错误
  • isLoading首次加载中(无缓存 + 请求中)
  • isValidating任何请求进行中(包括重新验证)
  • mutate:手动触发数据重新验证或更新缓存

stale-while-revalidate 策略

SWR 的核心工作流程:

这意味着用户几乎总是立即看到内容(来自缓存),同时数据在后台静默更新。

全局配置

通过 SWRConfig 统一配置所有 useSWR 的默认行为:

app/providers.tsx
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 为 nullfalse 或抛出异常时,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 同步:

components/TodoList.tsx
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>
);
}

分页与无限加载

hooks/useInfiniteList.ts
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 install @tanstack/react-query
app/providers.tsx
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>
);
}
hooks/useUser.ts
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(数据变更)

hooks/useCreateTodo.ts
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>
);
}

无限查询

hooks/useInfinitePosts.ts
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 定义,所有端点在一个地方声明:

store/api.ts
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;
components/TodoList.tsx
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 的标签失效机制

RTK Query 使用 providesTagsinvalidatesTags 实现自动缓存失效:

  • Query 提供标签(providesTags):声明这个查询提供了哪些数据
  • Mutation 使标签失效(invalidatesTags):声明这个变更影响了哪些数据
  • 当标签失效时,所有提供该标签的查询自动重新请求

这种声明式的缓存失效比手动 invalidateQueries 更容易维护。

方案对比

SWR vs TanStack Query

特性SWRTanStack Query
体积~4KB~13KB
缓存策略stale-while-revalidatestale-while-revalidate + 精细控制
MutationuseSWRMutationuseMutation(功能更丰富)
无限查询useSWRInfiniteuseInfiniteQuery(API 更直观)
DevTools社区版官方内置
离线支持基础完整的离线模式
SSR/RSC支持完整支持(Hydration、Prefetch)
垃圾回收无自动回收gcTime 自动回收
PrefetchpreloadprefetchQuery(更多选项)
依赖查询条件 keyenabled 选项
并行查询多个 HookuseQueries 批量查询
选择器select 选项
取消请求无内置AbortSignal 集成
框架支持ReactReact、Vue、Solid、Svelte、Angular

选型建议

选型总结
  • SWR:轻量级、API 简洁、Vercel/Next.js 生态首选,适合中小型项目
  • TanStack Query:功能最全面、DevTools 好用、社区大,适合中大型项目
  • RTK Query:已用 Redux 的项目直接用它,避免引入额外库
  • Apollo Client:GraphQL 项目的标配

Server Components 中的数据请求

React Server Components 中,数据请求回归到最简单的形式 —— 直接在组件中 await

app/users/[id]/page.tsx
// 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} />;
}
何时仍需 SWR/TanStack Query

Server Components 适合静态或不常变化的数据。以下场景仍需客户端请求库:

  • 需要实时更新的数据(聊天、通知)
  • 用户交互触发的请求(搜索、筛选)
  • 乐观更新(点赞、收藏)
  • 无限滚动/分页
  • 需要离线支持的 PWA

常见面试问题

Q1: SWR 的 stale-while-revalidate 策略是什么?

答案

stale-while-revalidate 源自 HTTP 缓存头 Cache-Control: max-age=1, stale-while-revalidate=59,核心思想是:

  1. 先返回缓存(stale):从缓存中立即返回可能过期的数据,用户无需等待
  2. 后台重新验证(revalidate):同时在后台发起真实请求获取最新数据
  3. 静默更新:如果新数据与缓存不同,更新缓存并触发 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 有两个优势:

  1. 结构化:可以将实体类型和参数分开,便于管理和失效
// 结构化的 key
['todos'] // 所有 todos
['todos', { status: 'done' }] // 筛选条件
['todos', 5] // 单个 todo

// 精确失效
queryClient.invalidateQueries({ queryKey: ['todos'] }); // 使所有 todos 相关缓存失效
queryClient.invalidateQueries({ queryKey: ['todos', 5] }); // 只失效 id=5 的
  1. 自动序列化比较:对象参数无论属性顺序如何,都能正确匹配
// 以下两个 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 + 后续客户端缓存

app/todos/page.tsx
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>
);
}
app/todos/TodoList.tsx
'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:

hooks/useRequest.ts
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
垃圾回收手动实现自动
DevToolsRedux DevTools专属 DevTools

核心优势是将服务端状态从全局 store 中剥离出来,让 Redux 专注于管理真正的客户端状态,各司其职。

相关链接