Dax 的博客

前端,全栈,独立开发进阶之路

SWR:人狠话不多的 React 数据请求 Hook

SWR 是一个为 React 打造的现代化数据请求库。它拥有精简的 API,内置了缓存、重新验证(Revalidation)以及请求去重功能。通过一个简单的 React Hook,它就能让你的 UI 保持快速、一致且永远处于最新状态。

一、 同类功能代码量对比

在 React 中请求后端接口并保持全局状态一致性是刚需。我们以获取 /api/user 为例,看看不同方案的实现成本。

1.1 Fetch + Zustand

需要手动管理 Loading、Error 和数据存储,逻辑较为琐碎。

// useUserStore.ts
import { useEffect } from 'react';
import { create } from 'zustand';

expoort const useUserStore = create((set, get) => ({
  user: null,
  isLoading: false,
  error: null,
  fetchUser: async () => {
    const { user, isLoading } = get();
    if (user || isLoading) return;

    set({ isLoading: true, error: null });
    try {
      const res = await fetch('/api/user');
      if (!res.ok) throw new Error('获取用户信息失败');
      const data = await res.json();
      set({ user: data, isLoading: false });
    } catch (err) {
      set({ error: err.message, isLoading: false });
    }
  },
}));

// UserProfile.tsx

export function UserProfile() {
  const { user, isLoading, error, fetchUser } = useUserStore();

  useEffect(() => {
    fetchUser();
  }, [fetchUser]);

  if (isLoading && !user) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;
  return <div>{user?.name}</div>;
}

1.2 React Query

功能强大,但配置成本较高。你需要定义 QueryClient,包裹 Provider,并手动声明 queryKey。另外技术文档也比较琐碎。

// APIProvider.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, 
      refetchOnWindowFocus: false,
    },
  },
});

export function APIProvider({ children }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}


// useUser.tsx
import { useQuery } from '@tanstack/react-query';

const fetchUser = async () => {
  const response = await fetch('/api/user');
  if (!response.ok) throw new Error('Network response was not ok');
  return response.json();
};

export function useUser() {
  return useQuery({
    queryKey: ['user'],
    queryFn: fetchUser,
  });
}


// UserProfile.tsx
export function UserProfile() {
  const { data: user, isLoading, error } = useUser();

  if (isLoading) return <div>正在加载用户信息...</div>;
  if (error) return <div>加载失败: {error.message}</div>;

  return (
    <div>
      <h1>用户信息</h1>
      <p>用户名: {user?.name}</p>
      <p>邮箱: {user?.email}</p>
    </div>
  );
}

1.3 SWR

极致简洁。

// useUser.ts
import useSWR from 'swr';

const fetcher = async (url) => {
  const response = await fetch(url);
  if (!response.ok) throw new Error('Network response was not ok');
  return response.json();
};

export function useUser() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher);
  
  return {
    user: data,
    isLoading,
    error
  };
}

// UseProfile.tsx

export function UserProfile() {
  const { user, isLoading, error } = useUser();

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{user?.name}</div>;
}

对比结论: 实现同样的功能,SWR 的心智负担和代码负担最小。虽然 React-Query 功能强大,但对于大部分常规需求,SWR 显得够用且简洁。

二、 设计哲学:触达本质

2.1 简洁优雅的品位

SWR 的 API 设计恰到好处。这种“少即是多”的设计感,体现了开发者的功底和品位。

2.2 URL 作为 Key 的独特设计

  • 资源的唯一标识: 在 Zustand 中我们需要手动命名存储变量,在 React Query 中需要维护 queryKey 数组。而 SWR 默认以 URL 作为缓存标识。
  • 回归本质: URL 本身就是 Uniform Resource Locator。它生来就是资源的唯一标识,做缓存 Key 再合适不过。

三、 变更操作:灵活的缓存策略

比起通常的请求库来说,SWR 默认开启了缓存策略。首先不要慌,这个不是心智负担,而是在常见场景下,可以更少代码实现更灵活的数据处理。

从实际使用看,使用 useSWR 实现请求查询一般没啥要关注的,需要关注的是使用 useSWRMutation 的变更类操作。先了解这两个配置:

  • populateCache:是否用变更请求的响应结果直接更新本地缓存。
  • revalidate:操作完成后,是否重新发起一次 GET 请求来校验数据。

3.1 重新验证模式 (Revalidate)

  • 配置:revalidate: true | populateCache: false
  • 过程: PATCH 成功 -> 发起 GET 请求 -> 更新 UI
  • 适用:体验有最多的延迟,但最稳妥,适用于后端逻辑复杂的场景。
const { trigger, isMutating } = useSWRMutation('/api/user', updateUserFetcher, {

  populateCache: false,

  revalidate: true,
});

3.2 响应填充模式 (Populate)

  • 配置:revalidate: false | populateCache: true
  • 过程: PATCH 成功并返回新数据 -> 使用 PATCH 返回的数据更新 UI
  • 适用:仅需一次网络往返,兼顾速度和一致性的中庸之选,适合常规操作。
function Profile() {
  const { trigger } = useSWRMutation('/api/user', updateUserFetcher, {
    populateCache: true, 
    revalidate: false, 
  });

  return <button onClick={() => trigger({ name: '新名字' })}>快速更新</button>;
}

3.3 乐观更新 (Optimistic Updates)

  • 配置:使用 optimisticData 配置 | populateCache: true | revalidate: false | rollbackOnError: true
  • 过程:点击瞬间更新 UI -> 发起 PATCH -> 成功后修正或失败后回滚。
  • 适用:更新速度最快,适用于点赞、评论等实时性要求极高的场景。
const { trigger, isMutating } = useSWRMutation('/api/user', updateUserFetcher, {
  optimisticData: (currentCache) => ({
    ...currentCache,
    ...currentUser,
    status: 'Saving...'
  }),
  populateCache: true,
  rollbackOnError: true,
  revalidate: false,
});

3.4 静默提交 (Silent Submission)

  • 配置:revalidate: false | populateCache: false
  • 过程: 发起删除或更新请求 -> UI 保持不变,不需要更新缓存和验证。
  • 适用:删除、埋点记录等不需要反馈在当前 UI 上的操作。
const { trigger, isMutating } = useSWRMutation('/api/user', deleteUserFetcher, {
  populateCache: false,
  revalidate: false
});

const handleDelete = async () => {
  if (window.confirm('确定要注销账号吗?')) {
    try {
      await trigger();
      alert('账号已在后台删除(静默模式)。');
    } catch (e) {
      alert('删除失败');
    }
  }
};

四、 结语

SWR 是一个在简洁和强大取得平衡的 React 数据请求管理 hook,不仅体积小,简单实用,也能在统一资源定位的技术理解上有所帮助。

就请求数据并缓存来说,那些需要繁琐配置 Key 的工具,仅仅把请求 URL 当作获取数据的方式,而 SWR 将其视为资源本身,体现出设计的品位和层次。类似 Unix 重那么多不同的设备终端,都抽象为“文件”来标识;Next.js 巧妙地用文件系统映射 URL。好的设计往往显得简单且更触达本质。