SWR vs React Query:全面比较 React 数据获取解决方案

SWR vs React Query:全面比较 React 数据获取解决方案

SWR

SWR 是由 Vercel 团队开发的轻量级数据获取 React Hooks 库。名称 "SWR" 来源于 HTTP 缓存策略 "stale-while-revalidate",这一策略允许应用立即从缓存(可能已过时)返回数据,同时在后台发送请求验证并最终使用最新数据更新页面。

React Query (TanStack Query)

React Query 是一个由 TanStack 维护的功能全面的异步状态管理库,现已更名为 TanStack Query。它不仅解决了数据获取问题,还提供了复杂的缓存管理、服务器状态同步以及数据更新等功能。

核心概念对比

特性 SWR React Query
核心理念 先返回缓存,后台重新验证 服务器状态与客户端状态分离管理
设计焦点 简单、轻量、高性能 全面的异步状态管理解决方案
体积 约 7kb (gzipped) 约 12kb (gzipped)
首次出现 2019年底 2019年中
社区支持 Vercel团队主导 TanStack社区支持
主要目标 提供简单直观的数据获取体验 提供完整的服务器状态管理解决方案

基本用法对比

SWR 基本用法

ts 复制代码
import useSWR from 'swr';

const fetcher = url => fetch(url).then(res => res.json());

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher);
  
  if (error) return <div>Failed to load</div>;
  if (isLoading) return <div>Loading...</div>;
  
  return <div>Hello, {data.name}!</div>;
}

React Query 基本用法

ts 复制代码
import { useQuery } from '@tanstack/react-query';

const fetchUser = () => fetch('/api/user').then(res => res.json());

function Profile() {
  const { data, error, isLoading } = useQuery({
    queryKey: ['user'],
    queryFn: fetchUser
  });
  
  if (error) return <div>Failed to load</div>;
  if (isLoading) return <div>Loading...</div>;
  
  return <div>Hello, {data.name}!</div>;
}

关键特性对比

缓存管理

SWR 缓存管理

SWR 使用基于键的简单缓存系统,每个请求都以其键为标识符存储在全局缓存中。

ts 复制代码
// 使用字符串键
const { data } = useSWR('/api/user', fetcher);

// 使用数组键(包含参数)
const { data } = useSWR(['/api/user', token], ([url, token]) => fetchWithToken(url, token));

SWR 的缓存持久策略相对简单,没有提供复杂的缓存配置选项。缓存在不使用时可能需要手动清理以避免内存泄漏。

ts 复制代码
const { cache } = useSWRConfig();
// 手动清除缓存
cache.delete('/api/user');
React Query 缓存管理

React Query 提供了更复杂的缓存系统,支持查询键、缓存时间控制和自动垃圾回收:

ts 复制代码
// 使用查询键
const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId)
});

// 配置缓存时间
const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  staleTime: 5 * 60 * 1000, // 5分钟内数据保持新鲜
  cacheTime: 10 * 60 * 1000 // 10分钟后从缓存中移除
});

React Query 自动管理缓存生命周期,可以精确控制数据何时过期、何时被移除。

自动重新验证

SWR 自动重新验证

SWR 提供了多种触发重新验证的方式:

ts 复制代码
const { data } = useSWR('/api/user', fetcher, {
  // 窗口聚焦时重新验证
  revalidateOnFocus: true,
  
  // 网络恢复时重新验证
  revalidateOnReconnect: true,
  
  // 定时轮询(每10秒)
  refreshInterval: 10000,
  
  // 切换到其他标签页再回来时,限制5分钟内只刷新一次
  focusThrottleInterval: 5 * 60 * 1000
});
React Query 自动重新验证

React Query 也提供了类似的自动重新获取机制:

ts 复制代码
const { data } = useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  
  // 窗口聚焦时重新获取
  refetchOnWindowFocus: true,
  
  // 网络恢复时重新获取
  refetchOnReconnect: true,
  
  // 定时轮询
  refetchInterval: 10000,
  
  // 仅当标签页处于活动状态时才轮询
  refetchIntervalInBackground: false
});

React Query 的自动重新获取配置更加灵活,可以通过 QueryClient 全局配置或针对特定查询定制。

数据更新与突变

SWR 数据更新

SWR 提供了两种主要的数据更新方式:mutateuseSWRMutation

ts 复制代码
// 全局 mutate
import { mutate } from 'swr';
mutate('/api/user', newData);

// 绑定数据的 mutate
const { data, mutate } = useSWR('/api/user', fetcher);
// 本地更新
mutate(newData, false); // 第二个参数为 false 表示不重新验证

// useSWRMutation 用于手动触发的操作
const { trigger } = useSWRMutation('/api/user', updateUser);

SWR 还支持乐观更新:

ts 复制代码
mutate('/api/todos', async todos => {
  const newTodo = { id: Date.now(), text: 'New Todo' };
  
  // 1. 更新本地数据立即反映在UI上
  const updatedTodos = [...todos, newTodo];
  
  // 2. 发送请求
  await fetch('/api/todos', {
    method: 'POST',
    body: JSON.stringify(newTodo)
  });
  
  // 3. 返回更新后的数据
  return updatedTodos;
}, { optimisticData: [...todos, newTodo], rollbackOnError: true });
React Query 数据更新

React Query 使用 useMutation 来处理数据更新操作:

ts 复制代码
const { mutate } = useMutation({
  mutationFn: (newUser) => updateUser(newUser),
  
  // 乐观更新
  onMutate: async (newUser) => {
    // 取消相关查询以避免冲突
    await queryClient.cancelQueries({ queryKey: ['user'] });
    
    // 保存之前的值
    const previousUser = queryClient.getQueryData(['user']);
    
    // 乐观更新缓存
    queryClient.setQueryData(['user'], newUser);
    
    // 返回回滚上下文
    return { previousUser };
  },
  
  // 如果失败,回滚到上一个状态
  onError: (err, newUser, context) => {
    queryClient.setQueryData(['user'], context.previousUser);
  },
  
  // 成功后使相关查询无效,触发重新获取
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['user'] });
  }
});

React Query 的突变 API 更加全面,提供了更多的生命周期回调和集成点。

API设计

SWR API设计

SWR 的 API 设计简洁直观,主要围绕 useSWRuseSWRMutation 两个 hooks:

ts 复制代码
// 基本用法
const { data, error, isLoading, isValidating, mutate } = useSWR(key, fetcher, options);

// 不可变数据
const { data } = useSWRImmutable(key, fetcher);

// 手动触发的数据更新
const { trigger, isMutating } = useSWRMutation(key, fetcher);

SWR 的 API 设计遵循简单性原则,大多数情况下只需要传递一个键和一个获取函数。

React Query API设计

React Query 提供了一套更全面的 API,包括查询和突变操作:

ts 复制代码
// 基本查询
const { data, error, isLoading, isFetching, ... } = useQuery(options);

// 无限查询(分页、加载更多)
const { data, fetchNextPage, hasNextPage, ... } = useInfiniteQuery(options);

// 突变操作
const { mutate, isLoading, isError, ... } = useMutation(options);

// 查询失效
queryClient.invalidateQueries({ queryKey: ['todos'] });

React Query 的 API 更加明确地区分了查询和突变操作,并提供了额外的工具来管理复杂的数据获取场景。

错误处理与重试机制

SWR 错误处理

SWR 提供了简单的错误处理和重试机制:

ts 复制代码
const { data, error } = useSWR('/api/user', fetcher, {
  onError: (err) => {
    console.error('请求失败:', err);
  },
  errorRetryCount: 3, // 失败后重试3次
  errorRetryInterval: 5000 // 每次重试间隔5秒
});
React Query 错误处理

React Query 提供了更强大的错误处理和重试功能:

ts 复制代码
const { data, error } = useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  onError: (err) => {
    console.error('请求失败:', err);
  },
  retry: 3, // 重试次数
  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // 指数退避
  useErrorBoundary: true // 错误可以被 React Error Boundary 捕获
});

React Query 还支持根据错误类型决定是否重试:

ts 复制代码
retry: (failureCount, error) => {
  if (error.status === 404) return false; // 不重试404错误
  return failureCount < 3; // 其他错误重试3次
}

性能优化

SWR 性能优化

SWR 通过几种方式来优化性能:

ts 复制代码
// 请求去重
// 相同时间内对同一键的多个请求会被合并
const { data: user1 } = useSWR('/api/user', fetcher);
const { data: user2 } = useSWR('/api/user', fetcher); // 不会发起新请求

// 使用 suspense 模式
const { data } = useSWR('/api/user', fetcher, { suspense: true });

// 预加载数据
import { preload } from 'swr';
preload('/api/user', fetcher);
React Query 性能优化

React Query 提供了更多性能优化选项:

ts 复制代码
// 查询函数只在特定条件下执行
const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  enabled: !!userId // 只有当 userId 存在时才执行查询
});

// 数据变换
const { data } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  select: (users) => users.filter(user => user.active) // 转换返回数据
});

// 预取数据
queryClient.prefetchQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId)
});

// 延迟数据请求
const { refetch } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  enabled: false // 初始不执行查询
});

React Query 还可以通过 setQueryDefaults 设置查询默认值,避免在多个组件中重复配置。

生态系统集成

SWR 生态系统

SWR 生态相对简单,主要由核心库和一些插件组成:

  • 与 Next.js 深度集成
  • swr/infinite 用于分页请求
  • swr/mutation 用于数据更新

SWR 被设计为轻量级解决方案,没有太多外部依赖。

React Query 生态系统

React Query 有更广泛的生态系统:

  • @tanstack/react-query-devtools 用于开发调试
  • 支持多种框架:Vue Query、Svelte Query 等
  • 插件系统支持扩展功能
  • 与其他 TanStack 库(如 TanStack Router)集成

React Query 还提供了一个强大的开发者工具,可以实时查看查询状态、缓存内容和请求详情。

使用场景分析

适合 SWR 的场景

  • 需要轻量级解决方案的项目
  • 与 Next.js 集成的应用
  • 简单的数据获取需求
  • 首屏加载性能至关重要的应用
  • 对包大小敏感的项目

适合 React Query 的场景

  • 复杂的数据管理需求
  • 需要处理分页、无限滚动等高级数据获取模式
  • 需要精细控制缓存策略的项目
  • 有大量数据更新和突变操作的应用
  • 需要强大开发者工具支持的团队
  • 使用多种状态管理模式的大型应用

源码实现比较

SWR 源码更为精简,主要围绕核心的数据获取和缓存逻辑。React Query 源码更为复杂,采用了更模块化的设计,支持更多高级功能。

两者在内部实现上有一些关键差异:

  • 缓存实现: SWR 使用简单的 Map 结构,React Query 使用更复杂的缓存系统,支持垃圾回收
  • 订阅机制: 两者都使用发布-订阅模式,但 React Query 实现了更精细的事件系统
  • 并发请求处理: React Query 有更复杂的去重和请求优先级处理
  • 类型系统: React Query 在 TypeScript 类型定义上更加完善

优缺点总结

SWR 优点

  1. 简单轻量: API 简洁直观,学习曲线平缓
  2. 性能优先: 包体积小,运行效率高
  3. 与 Next.js 深度集成: 在 Next.js 项目中使用体验极佳
  4. 自动重新验证: 内置多种数据更新策略
  5. 即时反馈: 乐观更新支持良好

SWR 缺点

  1. 功能相对有限: 缺少一些复杂数据管理功能
  2. 缓存管理简单: 没有提供复杂的缓存控制选项
  3. 开发工具缺乏: 没有内置的开发者调试工具
  4. 全局 key 命名问题: 可能导致命名冲突
  5. 没有请求中断 API: 无法优雅地取消请求
  6. 缺少 getter 方法: 无法在组件外部直接访问缓存数据

React Query 优点

  1. 功能全面: 支持几乎所有数据获取场景
  2. 缓存控制精细: 提供丰富的缓存配置选项
  3. 开发工具强大: 内置调试工具帮助开发和排查问题
  4. 查询无效化机制: 可以精确控制数据何时需要重新获取
  5. 强大的突变 API: 完整的生命周期钩子支持各种数据更新场景
  6. 类型支持优秀: TypeScript 定义全面

React Query 缺点

  1. 学习曲线较陡: 概念和 API 较多,需要时间掌握
  2. 包体积较大: 相比 SWR 体积更大
  3. 配置选项过多: 有时需要更多的初始设置
  4. 可能过度设计: 简单场景使用可能显得繁琐
  5. 不适合小型项目: 对于简单需求可能是杀鸡用牛刀

实践案例

使用 SWR 实现待办事项应用

ts 复制代码
import useSWR, { useSWRConfig } from 'swr';
import { useState } from 'react';

// 定义获取函数
const fetcher = url => fetch(url).then(res => res.json());

// 创建 Todo
async function addTodo(url, { arg }) {
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(arg)
  });
  
  if (!res.ok) throw new Error('添加失败');
  return res.json();
}

function TodoList() {
  const [newTodo, setNewTodo] = useState('');
  const { mutate } = useSWRConfig();
  
  // 获取待办事项列表
  const { data: todos = [], error, isLoading } = useSWR('/api/todos', fetcher);
  
  const handleAddTodo = async (e) => {
    e.preventDefault();
    
    const newTodoItem = { id: Date.now(), title: newTodo, completed: false };
    
    // 乐观更新
    mutate('/api/todos', async (currentTodos) => {
      try {
        // 先更新本地数据
        const updatedTodos = [...currentTodos, newTodoItem];
        
        // 发送请求
        await fetch('/api/todos', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(newTodoItem)
        });
        
        setNewTodo('');
        return updatedTodos;
      } catch (error) {
        // 发生错误时不更新
        return currentTodos;
      }
    }, { optimisticData: [...todos, newTodoItem], revalidate: false });
  };
  
  if (error) return <div>Failed to load todos</div>;
  if (isLoading) return <div>Loading...</div>;
  
  return (
    <div>
      <h1>Todo List</h1>
      
      <form onSubmit={handleAddTodo}>
        <input
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="添加新待办..."
        />
        <button type="submit">添加</button>
      </form>
      
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </div>
  );
}

使用 React Query 实现相同功能

ts 复制代码
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';

// 定义获取函数
const fetchTodos = async () => {
  const res = await fetch('/api/todos');
  if (!res.ok) throw new Error('Failed to fetch');
  return res.json();
};

// 创建 Todo 函数
const addTodo = async (newTodo) => {
  const res = await fetch('/api/todos', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(newTodo)
  });
  
  if (!res.ok) throw new Error('添加失败');
  return res.json();
};

function TodoList() {
  const [newTodo, setNewTodo] = useState('');
  const queryClient = useQueryClient();
  
  // 获取待办事项
  const { data: todos = [], isLoading, error } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos
  });
  
  // 添加待办事项的突变
  const addMutation = useMutation({
    mutationFn: addTodo,
    onMutate: async (newTodoItem) => {
      // 取消正在进行的请求
      await queryClient.cancelQueries({ queryKey: ['todos'] });
      
      // 保存当前数据
      const previousTodos = queryClient.getQueryData(['todos']);
      
      // 乐观更新UI
      queryClient.setQueryData(['todos'], old => [...old, newTodoItem]);
      
      // 返回之前的数据用于回滚
      return { previousTodos };
    },
    onError: (err, newTodo, context) => {
      // 发生错误时回滚到之前的数据
      queryClient.setQueryData(['todos'], context.previousTodos);
    },
    onSettled: () => {
      // 请求完成后刷新数据
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    }
  });
  
  const handleAddTodo = (e) => {
    e.preventDefault();
    
    const newTodoItem = { id: Date.now(), title: newTodo, completed: false };
    addMutation.mutate(newTodoItem);
    setNewTodo('');
  };
  
  if (error) return <div>Failed to load todos</div>;
  if (isLoading) return <div>Loading...</div>;
  
  return (
    <div>
      <h1>Todo List</h1>
      
      <form onSubmit={handleAddTodo}>
        <input
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="添加新待办..."
        />
        <button type="submit" disabled={addMutation.isPending}>
          {addMutation.isPending ? '添加中...' : '添加'}
        </button>
      </form>
      
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </div>
  );
}

如何选择合适的库

  1. 项目规模与复杂度:

    • 小型项目或简单数据需求: SWR
    • 大型项目或复杂数据管理需求: React Query
  2. 团队熟悉度:

    • 希望简单易上手: SWR
    • 愿意投入时间学习更强大的工具: React Query
  3. 特定功能需求:

    • 需要强大的开发者工具: React Query
    • 需要精细的缓存控制: React Query
    • 追求轻量级实现: SWR
    • 与 Next.js 深度集成: SWR
  4. 性能考量:

    • 注重首屏加载速度和包体积: SWR
    • 需要处理大量数据和复杂的查询关系: React Query

无论选择哪个库,都建议遵循以下最佳实践:

  1. 封装数据获取逻辑: 创建自定义 hooks 封装特定资源的获取
  2. 统一错误处理: 设置全局错误处理策略
  3. 合理设置缓存策略: 根据数据特性设置适当的缓存和重新验证策略
  4. 利用预加载: 提前加载可能需要的数据
  5. 合理使用乐观更新: 提高用户体验的同时确保数据一致性
  6. 避免过度重新获取: 调整重新验证的条件和频率
相关推荐
痴心阿文8 小时前
React如何导入md5,把密码password进行md5加密
前端·javascript·react.js
lryh_9 小时前
Vue 和 React 使用ref
javascript·vue.js·react.js·ref·forwardref
祈澈菇凉10 小时前
如何使用React Router处理404错误页面?
前端·javascript·react.js
MurphyChen11 小时前
🧭 React 组件通信指南:父传子、子传父、任意组件通信
前端·react.js·nuxt.js
Tonychen13 小时前
【React 源码阅读】useCallback
前端·react.js
我不是迈巴赫13 小时前
项目亮点万金油:自定义SSR水合保护hooks
前端·javascript·react.js
我认不到你15 小时前
油候插件、idea、VsCode插件推荐(自用)
java·前端·vscode·react.js·typescript·编辑器·intellij-idea
疏狂难除16 小时前
尝试使用tauri2+Django+React的项目
前端·react.js·前端框架
风无雨19 小时前
react 中 key 的使用
前端·react.js·前端框架
阿豪啊20 小时前
React入门-使用 Vite 搭建基于 React 和 TypeScript 的后台管理应用(一)
前端·react.js