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

2,045 阅读11分钟

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

SWR

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

React Query (TanStack Query)

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

核心概念对比

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

基本用法对比

SWR 基本用法

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 基本用法

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 使用基于键的简单缓存系统,每个请求都以其键为标识符存储在全局缓存中。

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

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

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

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

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

// 使用查询键
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 提供了多种触发重新验证的方式:

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

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

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

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

数据更新与突变

SWR 数据更新

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

// 全局 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 还支持乐观更新:

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 来处理数据更新操作:

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:

// 基本用法
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,包括查询和突变操作:

// 基本查询
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 提供了简单的错误处理和重试机制:

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

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

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 还支持根据错误类型决定是否重试:

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

性能优化

SWR 性能优化

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

// 请求去重
// 相同时间内对同一键的多个请求会被合并
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 提供了更多性能优化选项:

// 查询函数只在特定条件下执行
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 实现待办事项应用

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 实现相同功能

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. 避免过度重新获取: 调整重新验证的条件和频率