tanstack中的react-query和SWR使用及对比

TanStack Query vs SWR 深度对比与最佳实践

目录

  • 概述
  • 核心概念对比
  • [TanStack Query 详解](#TanStack Query 详解 "#tanstack-query-%E8%AF%A6%E8%A7%A3")
  • [TanStack Query 缓存机制](#TanStack Query 缓存机制 "#tanstack-query-%E7%BC%93%E5%AD%98%E6%9C%BA%E5%88%B6")
  • [SWR 详解](#SWR 详解 "#swr-%E8%AF%A6%E8%A7%A3")
  • [SWR 缓存机制](#SWR 缓存机制 "#swr-%E7%BC%93%E5%AD%98%E6%9C%BA%E5%88%B6")
  • 性能对比分析
  • 使用场景选择
  • 最佳实践
  • 迁移指南

概述

在现代 React 应用开发中,数据获取和状态管理是核心需求。TanStack Query(原 React Query)和 SWR 是两个最流行的数据获取库,它们都提供了缓存、重新验证、错误处理等功能,但在设计理念和使用方式上存在差异。

核心价值对比

特性 TanStack Query SWR
设计理念 功能丰富的数据管理平台 轻量级数据获取库
学习曲线 中等到复杂 简单到中等
包大小 较大 (~39KB) 较小 (~24KB)
生态系统 丰富的插件和工具 简洁的核心功能
TypeScript 优秀的类型支持 良好的类型支持

核心概念对比

数据获取模式

graph TD A[用户触发] --> B{选择数据获取库} B -->|TanStack Query| C[useQuery Hook] B -->|SWR| D[useSWR Hook] C --> E[Query Key管理] C --> F[Query Function执行] C --> G[缓存策略] D --> H[SWR Key管理] D --> I[Fetcher函数执行] D --> J[缓存与重新验证] E --> K[返回查询状态] H --> K style C fill:#e1f5fe style D fill:#f3e5f5

TanStack Query 详解

基础使用

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

// 基础查询示例
function UserProfile({ userId }) {
  const { 
    data: user, 
    isLoading, 
    error, 
    isError,
    refetch 
  } = useQuery({
    queryKey: ['user', userId],
    queryFn: async () => {
      const response = await fetch(`/api/users/${userId}`);
      if (!response.ok) {
        throw new Error('获取用户信息失败');
      }
      return response.json();
    },
    // 缓存时间
    staleTime: 5 * 60 * 1000, // 5分钟
    // 缓存垃圾回收时间
    cacheTime: 10 * 60 * 1000, // 10分钟
    // 重试配置
    retry: (failureCount, error) => {
      if (error.status === 404) return false;
      return failureCount < 3;
    },
    // 重试延迟
    retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
  });

  if (isLoading) return <UserSkeleton />;
  if (isError) return <ErrorMessage error={error} onRetry={refetch} />;

  return (
    <div className="user-profile">
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

高级查询功能

javascript 复制代码
// 并行查询
function Dashboard({ userId }) {
  const userQuery = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId)
  });

  const postsQuery = useQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchUserPosts(userId),
    // 依赖查询 - 等用户数据加载完成
    enabled: !!userQuery.data
  });

  const statsQuery = useQuery({
    queryKey: ['stats', userId],
    queryFn: () => fetchUserStats(userId),
    enabled: !!userQuery.data
  });

  const queries = [userQuery, postsQuery, statsQuery];
  const isLoading = queries.some(query => query.isLoading);
  const hasError = queries.some(query => query.isError);

  if (isLoading) return <DashboardSkeleton />;
  if (hasError) return <ErrorBoundary />;

  return (
    <div className="dashboard">
      <UserInfo user={userQuery.data} />
      <PostsList posts={postsQuery.data} />
      <StatsWidget stats={statsQuery.data} />
    </div>
  );
}

// 无限查询
function PostsList() {
  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: async ({ pageParam = 0 }) => {
      const response = await fetch(`/api/posts?page=${pageParam}&limit=10`);
      return response.json();
    },
    getNextPageParam: (lastPage, pages) => {
      return lastPage.hasMore ? pages.length : undefined;
    },
  });

  const posts = data?.pages.flatMap(page => page.posts) ?? [];

  return (
    <div className="posts-list">
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
      
      {hasNextPage && (
        <button
          onClick={() => fetchNextPage()}
          disabled={isFetchingNextPage}
          className="load-more-btn"
        >
          {isFetchingNextPage ? '加载中...' : '加载更多'}
        </button>
      )}
      
      {isFetching && !isFetchingNextPage && <LoadingSpinner />}
    </div>
  );
}

变更操作

javascript 复制代码
// 数据变更和乐观更新
function PostEditor({ postId, onSuccess }) {
  const queryClient = useQueryClient();
  
  const { data: post } = useQuery({
    queryKey: ['post', postId],
    queryFn: () => fetchPost(postId)
  });

  const updateMutation = useMutation({
    mutationFn: async (updatedPost) => {
      const response = await fetch(`/api/posts/${postId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(updatedPost)
      });
      if (!response.ok) throw new Error('更新失败');
      return response.json();
    },
    // 乐观更新
    onMutate: async (newPost) => {
      // 取消正在进行的查询
      await queryClient.cancelQueries({ queryKey: ['post', postId] });
      
      // 保存之前的数据用于回滚
      const previousPost = queryClient.getQueryData(['post', postId]);
      
      // 乐观更新
      queryClient.setQueryData(['post', postId], {
        ...previousPost,
        ...newPost,
        updatedAt: new Date().toISOString()
      });
      
      return { previousPost };
    },
    // 成功时更新相关查询
    onSuccess: (data) => {
      queryClient.setQueryData(['post', postId], data);
      // 更新列表数据
      queryClient.invalidateQueries({ queryKey: ['posts'] });
      onSuccess?.(data);
    },
    // 错误时回滚
    onError: (err, newPost, context) => {
      if (context?.previousPost) {
        queryClient.setQueryData(['post', postId], context.previousPost);
      }
    },
    // 无论成功失败都重新获取数据
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['post', postId] });
    },
  });

  const handleSubmit = (formData) => {
    updateMutation.mutate(formData);
  };

  if (!post) return <PostSkeleton />;

  return (
    <PostForm
      initialData={post}
      onSubmit={handleSubmit}
      isSubmitting={updateMutation.isPending}
      error={updateMutation.error}
    />
  );
}

高级配置和中间件

javascript 复制代码
// 全局配置
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // 全局查询配置
      staleTime: 5 * 60 * 1000,
      cacheTime: 10 * 60 * 1000,
      retry: (failureCount, error) => {
        // 全局重试逻辑
        if (error.status >= 400 && error.status < 500) {
          return false; // 客户端错误不重试
        }
        return failureCount < 3;
      },
      // 全局错误处理
      onError: (error) => {
        console.error('Query Error:', error);
        if (error.status === 401) {
          // 处理认证错误
          window.location.href = '/login';
        }
      },
    },
    mutations: {
      // 全局变更配置
      onError: (error) => {
        console.error('Mutation Error:', error);
        // 显示错误提示
        toast.error(error.message);
      },
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Router>
        <Routes>
          {/* 路由配置 */}
        </Routes>
      </Router>
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

TanStack Query 缓存机制

TanStack Query 提供了强大而灵活的缓存系统,能够智能地管理数据的生命周期,提供优秀的用户体验。

能否判断是否是否新鲜?是否过期?

TanStack Query(React Query)之所以能判断缓存是否"新鲜"或"过期" ,是因为它对每一条缓存数据都维护了完整的生命周期元信息(metadata) ,并且采用了 时间驱动的缓存策略

TanStack Query 会记录每条缓存的"最后更新时间",并对照配置项判断数据是否还"新鲜"或"过期"。

  • 每条缓存都有精确的更新时间(dataUpdatedAt
  • 配合用户设定的 staleTimecacheTime
  • 系统自动判断缓存是否 stale 或 expired,决定是否重新 fetch 或垃圾回收

生命周期元信息

生命周期元信息(metadata),指的是它为每一个 query 自动维护的一组状态与时间戳,这些信息帮助它判断:

  • 缓存是否**新鲜(fresh)*还是*陈旧(stale)
  • 缓存是否应该重新请求(refetch)
  • 缓存是否可以被自动清除(garbage collected)
  • Query 当前状态是 loading、success、error,还是 idle
  • 是否在被使用中(active)
生命周期元信息包含哪些?

每一个 Query 实例内部会维护类似以下结构:

ts 复制代码
{
  queryKey: ['todos'],
  state: {
    data,                    // 当前缓存的数据
    dataUpdatedAt,           // 上次 fetch 成功的时间戳(用于 stale 判断)
    error,                   // 最近一次请求的错误
    errorUpdatedAt,          // 错误时间
    fetchStatus,             // 'idle' | 'fetching' | 'paused'
    status,                  // 'loading' | 'success' | 'error'
    isInvalidated,           // 是否被标记为"无效"------触发 refetch
    isPaused,                // 当前是否暂停请求
    fetchMeta                // 请求的一些附加信息,如 retry 次数等
  },
  observers: [...],          // 哪些组件正在使用它(用于 active 判断)
  gcTimeout,                 // GC 回收的定时器
  options: { staleTime, cacheTime, ... }
}

关键字段说明
字段名 含义
dataUpdatedAt 上次请求成功的时间戳,用于判断是否 stale
status 当前 query 状态:loading / success / error
fetchStatus 是否正在请求中:fetching / idle
isInvalidated 被标记为"无效",会触发下次自动刷新
observers 有多少组件在"使用"这个 query,决定是否需要 GC
gcTimeout 缓存未使用后触发的垃圾回收计时器

生命周期流程简图
flowchart TD A[初次 useQuery 请求] --> B[fetch 成功] B --> C[记录 dataUpdatedAt 时间戳] C --> D{超过 staleTime?} D -- 否 --> E[认为 fresh,不触发 refetch] D -- 是 --> F[标记为 stale,下次激活会 refetch] F --> G{有组件在用?} G -- 否 --> H[启动 GC 计时器,等待 cacheTime] H --> I[清除缓存数据]

应用场景:为什么需要这些 metadata?
自动刷新数据的时机判断
ts 复制代码
if (Date.now() - dataUpdatedAt > staleTime) {
  // 数据是 stale 的,触发 refetch
}
判断是否有组件在用(决定是否 GC)
ts 复制代码
if (query.observers.length === 0) {
  // 启动 GC 计时器,准备删除缓存
}
控制 UI 状态反馈
ts 复制代码
if (status === 'loading') showSpinner()
if (status === 'error') showError()

想看 metadata?React Query Devtools!

如果你使用 React Query Devtools,可以看到每个 query 的元信息:

  • Data
  • isStale
  • UpdatedAt 时间
  • 状态字段
  • 是否 active
  • 是否正在 fetching

示例图:

yaml 复制代码
 Query ['todos']
- Status: success
- isStale: true
- Data updated at: 12:03:12 PM
- Active observers: 1
- Fetching: false

TanStack Query 的 生命周期元信息(metadata) 是一整套用于管理缓存生命周期的状态和时间戳数据,它支撑了 query 的核心功能,包括:

  • 是否需要重新请求?
  • 数据是否还新鲜?
  • 缓存是否可以清除?
  • UI 如何正确反馈加载、错误状态?

这些 metadata 让 TanStack Query 成为"智能数据管理库"而不仅仅是"数据请求库"。

缓存生命周期

graph TD A[查询触发] --> B{缓存中有数据?} B -->|有| C{数据是否新鲜?} B -->|无| D[发起网络请求] C -->|新鲜| E[返回缓存数据] C -->|过期| F[返回缓存数据 + 后台更新] D --> G[获取新数据] F --> G G --> H[更新缓存] H --> I[通知组件更新] I --> J{还有组件使用?} J -->|有| K[保持活跃] J -->|无| L[开始垃圾回收计时] L --> M[超时后清除缓存] style E fill:#e8f5e8 style F fill:#fff3cd style M fill:#f8d7da

缓存状态管理

javascript 复制代码
// 缓存状态示例
function CacheStatusDemo() {
  const queryClient = useQueryClient();
  
  const { data, status, fetchStatus, dataUpdatedAt } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    staleTime: 5 * 60 * 1000, // 5分钟内认为数据新鲜
    cacheTime: 30 * 60 * 1000, // 30分钟后垃圾回收
  });

  // 查询状态详解
  const queryState = {
    // status: 查询的状态
    // 'pending' - 首次加载中
    // 'error' - 查询失败
    // 'success' - 查询成功
    status,
    
    // fetchStatus: 网络请求状态
    // 'idle' - 没有网络请求
    // 'fetching' - 正在请求
    fetchStatus,
    
    // 数据相关
    hasData: !!data,
    dataUpdatedAt: new Date(dataUpdatedAt).toLocaleString(),
  };

  return (
    <div className="cache-demo">
      <h3>缓存状态监控</h3>
      <pre>{JSON.stringify(queryState, null, 2)}</pre>
      
      <div className="actions">
        <button onClick={() => queryClient.invalidateQueries({ queryKey: ['posts'] })}>
          使缓存失效
        </button>
        <button onClick={() => queryClient.removeQueries({ queryKey: ['posts'] })}>
          清除缓存
        </button>
        <button onClick={() => queryClient.resetQueries({ queryKey: ['posts'] })}>
          重置查询
        </button>
      </div>
    </div>
  );
}

缓存配置详解

javascript 复制代码
// 详细的缓存配置
const cacheConfig = {
  // 数据新鲜度配置
  staleTime: 5 * 60 * 1000, // 5分钟内数据被认为是新鲜的
  
  // 缓存时间配置
  cacheTime: 30 * 60 * 1000, // 数据在内存中保存30分钟
  
  // 重新获取策略
  refetchOnMount: true, // 组件挂载时重新获取
  refetchOnWindowFocus: true, // 窗口获得焦点时重新获取
  refetchOnReconnect: true, // 网络重连时重新获取
  
  // 重试配置
  retry: (failureCount, error) => {
    if (error.status === 404) return false;
    return failureCount < 3;
  },
  retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
};

// 应用缓存配置
function PostsWithCache() {
  const { data: posts, isStale, isFetching } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    ...cacheConfig,
  });

  return (
    <div className="posts-container">
      <div className="cache-indicators">
        {isStale && <span className="stale-badge">数据已过期</span>}
        {isFetching && <span className="fetching-badge">更新中...</span>}
      </div>
      <PostsList posts={posts} />
    </div>
  );
}

缓存更新策略

javascript 复制代码
// 手动缓存更新
function CacheUpdateStrategies() {
  const queryClient = useQueryClient();
  
  // 策略1: 直接设置缓存数据
  const setUserData = (userId, userData) => {
    queryClient.setQueryData(['user', userId], userData);
  };
  
  // 策略2: 函数式更新
  const updateUserLikes = (userId) => {
    queryClient.setQueryData(['user', userId], (oldData) => {
      if (!oldData) return oldData;
      return {
        ...oldData,
        likes: oldData.likes + 1,
        updatedAt: new Date().toISOString(),
      };
    });
  };
  
  // 策略3: 批量更新相关缓存
  const updateUserAndPosts = async (userId, newUserData) => {
    // 更新用户数据
    queryClient.setQueryData(['user', userId], newUserData);
    
    // 更新相关的文章列表缓存
    queryClient.setQueryData(['posts', 'user', userId], (oldPosts) => {
      if (!oldPosts) return oldPosts;
      return oldPosts.map(post => ({
        ...post,
        author: newUserData,
      }));
    });
    
    // 使相关查询失效
    queryClient.invalidateQueries({ queryKey: ['posts', 'feed'] });
  };
  
  // 策略4: 条件性缓存更新
  const smartCacheUpdate = (data) => {
    queryClient.setQueriesData(
      { queryKey: ['posts'] },
      (oldData) => {
        // 只更新包含特定条件的缓存
        if (oldData?.meta?.category === data.category) {
          return {
            ...oldData,
            posts: [...oldData.posts, data],
          };
        }
        return oldData;
      }
    );
  };
  
  return {
    setUserData,
    updateUserLikes,
    updateUserAndPosts,
    smartCacheUpdate,
  };
}

缓存预取和预填充

javascript 复制代码
// 缓存预取策略
function CachePrefetching() {
  const queryClient = useQueryClient();
  
  // 预取下一页数据
  const prefetchNextPage = async (currentPage) => {
    const nextPage = currentPage + 1;
    
    // 预取下一页,但不会触发加载状态
    await queryClient.prefetchQuery({
      queryKey: ['posts', 'page', nextPage],
      queryFn: () => fetchPosts({ page: nextPage }),
      staleTime: 5 * 60 * 1000,
    });
  };
  
  // 鼠标悬停时预取详情
  const prefetchPostDetail = async (postId) => {
    await queryClient.prefetchQuery({
      queryKey: ['post', postId],
      queryFn: () => fetchPost(postId),
      staleTime: 10 * 60 * 1000,
    });
  };
  
  // 预填充相关数据
  const prefillUserData = (users) => {
    users.forEach(user => {
      queryClient.setQueryData(['user', user.id], user);
    });
  };
  
  return (
    <div className="posts-list">
      {posts.map((post, index) => (
        <div
          key={post.id}
          onMouseEnter={() => prefetchPostDetail(post.id)}
          className="post-item"
        >
          <PostCard post={post} />
          {index === posts.length - 5 && prefetchNextPage(currentPage)}
        </div>
      ))}
    </div>
  );
}

缓存持久化

javascript 复制代码
import { persistQueryClient } from '@tanstack/react-query-persist-client-core';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';

// 配置持久化存储
const persister = createSyncStoragePersister({
  storage: window.localStorage,
  key: 'REACT_QUERY_OFFLINE_CACHE',
  serialize: JSON.stringify,
  deserialize: JSON.parse,
});

// 创建支持持久化的 QueryClient
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      cacheTime: 1000 * 60 * 60 * 24, // 24小时
      staleTime: 1000 * 60 * 5, // 5分钟
    },
  },
});

// 初始化持久化
persistQueryClient({
  queryClient,
  persister,
  maxAge: 1000 * 60 * 60 * 24, // 24小时
  buster: '1.0', // 版本控制
});

// 选择性持久化
function SelectivePersistence() {
  const { data } = useQuery({
    queryKey: ['user-settings'],
    queryFn: fetchUserSettings,
    // 这个查询会被持久化
    meta: {
      persist: true,
    },
  });
  
  const { data: tempData } = useQuery({
    queryKey: ['temp-data'],
    queryFn: fetchTempData,
    // 这个查询不会被持久化
    meta: {
      persist: false,
    },
  });
  
  return <div>{/* 组件内容 */}</div>;
}

缓存调试和监控

javascript 复制代码
// 缓存调试工具
function CacheDebugger() {
  const queryClient = useQueryClient();
  const queryCache = queryClient.getQueryCache();
  
  // 获取所有缓存状态
  const getAllCacheInfo = () => {
    const queries = queryCache.getAll();
    return queries.map(query => ({
      queryKey: query.queryKey,
      state: query.state.status,
      dataUpdatedAt: query.state.dataUpdatedAt,
      errorUpdatedAt: query.state.errorUpdatedAt,
      fetchStatus: query.state.fetchStatus,
      isStale: query.isStale(),
      observersCount: query.getObserversCount(),
    }));
  };
  
  // 缓存性能监控
  const monitorCachePerformance = () => {
    const observer = {
      onQueryAdded: (query) => {
        console.log('Query Added:', query.queryKey);
      },
      onQueryRemoved: (query) => {
        console.log('Query Removed:', query.queryKey);
      },
      onQueryUpdated: (query) => {
        console.log('Query Updated:', query.queryKey, {
          fetchStatus: query.state.fetchStatus,
          status: query.state.status,
        });
      },
    };
    
    // 订阅缓存事件
    const unsubscribe = queryCache.subscribe(observer);
    
    return unsubscribe;
  };
  
  const [cacheInfo, setCacheInfo] = useState([]);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setCacheInfo(getAllCacheInfo());
    }, 1000);
    
    const unsubscribe = monitorCachePerformance();
    
    return () => {
      clearInterval(interval);
      unsubscribe();
    };
  }, []);
  
  return (
    <div className="cache-debugger">
      <h3>缓存监控面板</h3>
      <table>
        <thead>
          <tr>
            <th>Query Key</th>
            <th>状态</th>
            <th>最后更新</th>
            <th>观察者数量</th>
            <th>是否过期</th>
          </tr>
        </thead>
        <tbody>
          {cacheInfo.map((info, index) => (
            <tr key={index}>
              <td>{JSON.stringify(info.queryKey)}</td>
              <td>{info.state}</td>
              <td>{new Date(info.dataUpdatedAt).toLocaleTimeString()}</td>
              <td>{info.observersCount}</td>
              <td>{info.isStale ? '是' : '否'}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

SWR 详解

基础使用

javascript 复制代码
import useSWR, { mutate } from 'swr';

// 全局 fetcher 函数
const fetcher = async (url) => {
  const response = await fetch(url);
  if (!response.ok) {
    const error = new Error('请求失败');
    error.status = response.status;
    throw error;
  }
  return response.json();
};

// 基础用法
function UserProfile({ userId }) {
  const { 
    data: user, 
    error, 
    isLoading,
    mutate: refreshUser 
  } = useSWR(`/api/users/${userId}`, fetcher, {
    // 重新验证策略
    revalidateOnFocus: true,
    revalidateOnReconnect: true,
    refreshInterval: 30000, // 30秒自动刷新
    // 错误重试
    errorRetryCount: 3,
    errorRetryInterval: 5000,
    // 缓存配置
    dedupingInterval: 2000,
  });

  if (error) return <ErrorMessage error={error} onRetry={refreshUser} />;
  if (isLoading) return <UserSkeleton />;

  return (
    <div className="user-profile">
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <button onClick={() => refreshUser()}>
        刷新数据
      </button>
    </div>
  );
}

高级功能

javascript 复制代码
// 条件查询和依赖查询
function Dashboard({ userId }) {
  // 用户基础信息
  const { data: user } = useSWR(
    userId ? `/api/users/${userId}` : null,
    fetcher
  );

  // 依赖查询 - 只有用户信息加载完成后才执行
  const { data: posts } = useSWR(
    user ? `/api/users/${userId}/posts` : null,
    fetcher
  );

  const { data: stats } = useSWR(
    user ? `/api/users/${userId}/stats` : null,
    fetcher
  );

  if (!user) return <DashboardSkeleton />;

  return (
    <div className="dashboard">
      <UserInfo user={user} />
      {posts && <PostsList posts={posts} />}
      {stats && <StatsWidget stats={stats} />}
    </div>
  );
}

// 分页数据处理
function PostsList() {
  const [page, setPage] = useState(1);
  const [allPosts, setAllPosts] = useState([]);

  const { data, error, isLoading } = useSWR(
    `/api/posts?page=${page}&limit=10`,
    fetcher,
    {
      onSuccess: (newData) => {
        if (page === 1) {
          setAllPosts(newData.posts);
        } else {
          setAllPosts(prev => [...prev, ...newData.posts]);
        }
      }
    }
  );

  const loadMore = () => {
    if (data?.hasMore) {
      setPage(prev => prev + 1);
    }
  };

  if (error) return <ErrorMessage error={error} />;

  return (
    <div className="posts-list">
      {allPosts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
      
      {data?.hasMore && (
        <button onClick={loadMore} disabled={isLoading}>
          {isLoading ? '加载中...' : '加载更多'}
        </button>
      )}
    </div>
  );
}

数据变更和乐观更新

"先更新界面,再请求服务器,失败再回滚。"

javascript 复制代码
// 乐观更新实现
function PostEditor({ postId }) {
  const { data: post, mutate } = useSWR(`/api/posts/${postId}`, fetcher);

  const updatePost = async (updatedData) => {
    try {
      // 乐观更新本地数据
      const optimisticData = { ...post, ...updatedData };
      mutate(optimisticData, false); // false = 不重新验证

      // 发送请求到服务器
      const response = await fetch(`/api/posts/${postId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(updatedData)
      });

      if (!response.ok) throw new Error('更新失败');

      const serverData = await response.json();
      
      // 用服务器数据更新缓存
      mutate(serverData);
      
      // 更新相关的缓存
      mutate('/api/posts'); // 刷新文章列表
      
    } catch (error) {
      // 错误时恢复原始数据
      mutate();
      throw error;
    }
  };

  const handleSubmit = async (formData) => {
    try {
      await updatePost(formData);
      toast.success('更新成功');
    } catch (error) {
      toast.error(error.message);
    }
  };

  if (!post) return <PostSkeleton />;

  return (
    <PostForm
      initialData={post}
      onSubmit={handleSubmit}
    />
  );
}

SWR 配置和中间件

javascript 复制代码
import { SWRConfig } from 'swr';

// 全局配置
function App() {
  return (
    <SWRConfig
      value={{
        fetcher,
        // 全局配置
        revalidateOnFocus: false,
        revalidateOnReconnect: true,
        refreshInterval: 0,
        dedupingInterval: 2000,
        // 全局错误处理
        onError: (error, key) => {
          console.error(`SWR Error for ${key}:`, error);
          if (error.status === 401) {
            window.location.href = '/login';
          }
        },
        // 全局成功处理
        onSuccess: (data, key) => {
          console.log(`SWR Success for ${key}`);
        },
        // 错误重试配置
        errorRetryCount: 3,
        errorRetryInterval: 5000,
        // 加载超时
        loadingTimeout: 3000,
        onLoadingSlow: (key) => {
          console.warn(`Slow loading for ${key}`);
        },
      }}
    >
      <Router>
        <Routes>
          {/* 路由配置 */}
        </Routes>
      </Router>
    </SWRConfig>
  );
}

SWR 缓存机制

SWR 的缓存机制以"stale-while-revalidate"模式为核心,提供了简洁而高效的数据管理方案。

SWR 缓存策略概览

graph TD A[用户请求数据] --> B{缓存中有数据?} B -->|有| C[立即返回缓存数据] B -->|无| D[显示loading状态] C --> E[后台重新验证数据] D --> F[发起网络请求] E --> G{数据有更新?} F --> H[获取最新数据] G -->|有| I[更新缓存和UI] G -->|无| J[保持当前数据] H --> K[更新缓存] K --> L[更新UI状态] style C fill:#e8f5e8 style E fill:#fff3cd style I fill:#cceeff

判断缓存是否新鲜?是否过期?

SWR 的设计理念围绕 数据"新鲜度"Stale-While-Revalidate 策略),它不是通过传统的"缓存过期时间"控制缓存,而是通过以下几种新鲜度控制机制来实现"数据始终保持接近最新"。

SWR 如何控制缓存"新鲜度"?

控制方式 是否过期缓存 说明
revalidateOnFocus ✅ 控制重新获取 页面聚焦时是否刷新数据(默认 true
revalidateOnReconnect ✅ 控制重新获取 网络重新连接时是否刷新数据(默认 true
refreshInterval ✅ 定时刷新 设置间隔时间,定时自动重新请求数据
dedupingInterval ⛔ 非缓存过期 在该时间内重复请求同一 key,会被忽略(防止频繁请求)
keepPreviousData ⛔ 保持旧缓存 用于分页/变化型 key 的场景,保持上一次数据
cacheProvider ⛔ 自定义缓存策略 若你想实现 TTL(过期控制),可以在自定义 provider 中处理

SWR 的缓存不是传统的 TTL 缓存

SWR 的缓存是 "永不过期,但可以随时更新",只要不手动或自动触发更新,它一直存在于缓存中(内存)。

所以:

  • 它并不会在 X 秒后自动清除缓存
  • 而是提供机制,在适当时机刷新数据
    • 页面重新聚焦
    • 网络恢复
    • 设置 refreshInterval
    • 手动 mutate
想实现"缓存过期"怎么办?

需要手动实现类似 TTL(Time-To-Live)的"定时清除缓存"的能力,可以结合 cacheProvider 自定义:

SWR 本身不使用"缓存过期"来控制缓存,而是通过"数据新鲜策略"(聚焦、连接恢复、定时刷新、手动 mutate 等)来保持缓存的数据始终是"接近最新"的。

示例:带过期时间的 localStorage 缓存方案
typescript 复制代码
ts


复制编辑
const EXPIRATION_MS = 5 * 60 * 1000 // 5分钟

const ttlCacheProvider = () => {
  const map = new Map<string, any>()

  const raw = localStorage.getItem('swr-ttl-cache')
  if (raw) {
    const entries: [string, { data: any, timestamp: number }][] = JSON.parse(raw)
    for (const [key, value] of entries) {
      if (Date.now() - value.timestamp < EXPIRATION_MS) {
        map.set(key, value.data)
      }
    }
  }

  window.addEventListener('beforeunload', () => {
    const entries = Array.from(map.entries()).map(([key, data]) => [key, { data, timestamp: Date.now() }])
    localStorage.setItem('swr-ttl-cache', JSON.stringify(entries))
  })

  return map
}

缓存键管理

javascript 复制代码
// SWR 缓存键策略
function SWRCacheKeyStrategies() {
  // 基础字符串键
  const { data: user } = useSWR('/api/user', fetcher);
  
  // 带参数的键
  const { data: posts } = useSWR(`/api/posts?page=${page}&limit=${limit}`, fetcher);
  
  // 数组形式的键(推荐)
  const { data: userPosts } = useSWR(['posts', 'user', userId], () => 
    fetcher(`/api/users/${userId}/posts`)
  );
  
  // 条件查询键
  const { data: profile } = useSWR(
    userId ? ['user', 'profile', userId] : null, // null 会暂停查询
    () => fetcher(`/api/users/${userId}/profile`)
  );
  
  // 复杂查询键
  const { data: searchResults } = useSWR(
    ['search', { query, filters, sort }],
    ([, params]) => fetcher('/api/search', { params })
  );
  
  return { user, posts, userPosts, profile, searchResults };
}

// 缓存键工厂函数
const cacheKeys = {
  users: {
    all: () => ['users'],
    byId: (id) => ['users', id],
    posts: (id) => ['users', id, 'posts'],
    profile: (id) => ['users', id, 'profile'],
  },
  posts: {
    all: () => ['posts'],
    byCategory: (category) => ['posts', 'category', category],
    search: (query) => ['posts', 'search', query],
  },
};

缓存配置和重新验证

javascript 复制代码
// 详细的缓存和重新验证配置
function SWRCacheConfiguration() {
  const { data, error, isLoading, mutate } = useSWR(
    '/api/user-data',
    fetcher,
    {
      // 重新验证策略
      revalidateOnFocus: true, // 窗口获得焦点时重新验证
      revalidateOnReconnect: true, // 网络重连时重新验证
      revalidateIfStale: true, // 数据过期时重新验证
      revalidateOnMount: true, // 组件挂载时重新验证
      
      // 自动刷新
      refreshInterval: 30000, // 每30秒自动刷新
      refreshWhenHidden: false, // 页面隐藏时不刷新
      refreshWhenOffline: false, // 离线时不刷新
      
      // 缓存相关
      dedupingInterval: 2000, // 2秒内相同请求去重
      focusThrottleInterval: 5000, // 焦点重新验证节流
      
      // 错误处理
      errorRetryCount: 3,
      errorRetryInterval: 5000,
      shouldRetryOnError: (error) => {
        // 404错误不重试
        return error.status !== 404;
      },
      
      // 性能优化
      suspense: false, // 是否使用Suspense
      fallbackData: undefined, // 初始数据
      keepPreviousData: true, // 保持之前的数据直到新数据到达
    }
  );
  
  return { data, error, isLoading, mutate };
}

手动缓存操作

javascript 复制代码
import { mutate, cache } from 'swr';

// 缓存操作工具集
function SWRCacheOperations() {
  // 获取缓存数据
  const getCachedData = (key) => {
    return cache.get(key);
  };
  
  // 设置缓存数据
  const setCacheData = (key, data) => {
    mutate(key, data, false); // false = 不重新验证
  };
  
  // 更新缓存数据
  const updateCacheData = (key, updateFn) => {
    mutate(key, updateFn, false);
  };
  
  // 删除缓存
  const deleteCache = (key) => {
    cache.delete(key);
  };
  
  // 清空所有缓存
  const clearAllCache = () => {
    cache.clear();
  };
  
  // 批量更新相关缓存
  const batchUpdateCache = (updates) => {
    updates.forEach(({ key, data, revalidate = false }) => {
      mutate(key, data, revalidate);
    });
  };
  
  // 条件性缓存更新
  const conditionalUpdate = (pattern, updateFn) => {
    // 遍历所有缓存键
    for (const key of cache.keys()) {
      if (typeof key === 'string' && key.includes(pattern)) {
        const currentData = cache.get(key);
        const newData = updateFn(currentData);
        if (newData !== currentData) {
          mutate(key, newData, false);
        }
      }
    }
  };
  
  return {
    getCachedData,
    setCacheData,
    updateCacheData,
    deleteCache,
    clearAllCache,
    batchUpdateCache,
    conditionalUpdate,
  };
}

乐观更新实现

javascript 复制代码
// 高级乐观更新模式
function useOptimisticSWR(key, fetcher, config = {}) {
  const { data, error, mutate } = useSWR(key, fetcher, config);
  
  // 乐观更新函数
  const optimisticUpdate = useCallback(async (
    updateData,
    requestFn,
    rollbackOnError = true
  ) => {
    // 保存当前数据用于回滚
    const previousData = data;
    
    try {
      // 立即更新UI
      mutate(updateData, false);
      
      // 执行实际请求
      const result = await requestFn();
      
      // 用服务器数据更新缓存
      mutate(result);
      
      return result;
    } catch (error) {
      // 错误时回滚数据
      if (rollbackOnError && previousData !== undefined) {
        mutate(previousData, false);
      }
      throw error;
    }
  }, [data, mutate]);
  
  return {
    data,
    error,
    mutate,
    optimisticUpdate,
  };
}

// 乐观更新示例
function TodoItem({ todo }) {
  const { data, optimisticUpdate } = useOptimisticSWR(
    ['todo', todo.id],
    () => fetchTodo(todo.id),
    { fallbackData: todo }
  );
  
  const toggleComplete = async () => {
    const optimisticData = {
      ...data,
      completed: !data.completed,
      updatedAt: new Date().toISOString(),
    };
    
    try {
      await optimisticUpdate(
        optimisticData,
        () => updateTodo(todo.id, { completed: !data.completed })
      );
      
      // 更新相关的todo列表缓存
      mutate(['todos'], (todos) => {
        return todos.map(t => 
          t.id === todo.id ? optimisticData : t
        );
      }, false);
      
    } catch (error) {
      toast.error('更新失败');
    }
  };
  
  return (
    <div className={`todo-item ${data.completed ? 'completed' : ''}`}>
      <input
        type="checkbox"
        checked={data.completed}
        onChange={toggleComplete}
      />
      <span>{data.title}</span>
    </div>
  );
}

缓存预填充和预取

javascript 复制代码
// SWR 预取策略
function SWRPrefetching() {
  // 预取数据
  const prefetchData = useCallback((key, fetcher) => {
    // 只有缓存中没有数据时才预取
    if (!cache.has(key)) {
      mutate(key, fetcher(), false);
    }
  }, []);
  
  // 预填充列表数据中的详情
  const prefillDetailsFromList = useCallback((listData) => {
    listData.forEach(item => {
      const detailKey = ['item', item.id];
      if (!cache.has(detailKey)) {
        mutate(detailKey, item, false);
      }
    });
  }, []);
  
  // 智能预取下一页
  const prefetchNextPage = useCallback((currentPage, hasMore) => {
    if (hasMore) {
      const nextPageKey = ['posts', 'page', currentPage + 1];
      prefetchData(nextPageKey, () => fetchPosts(currentPage + 1));
    }
  }, [prefetchData]);
  
  return {
    prefetchData,
    prefillDetailsFromList,
    prefetchNextPage,
  };
}

// 鼠标悬停预取示例
function PostCard({ post }) {
  const { prefetchData } = SWRPrefetching();
  
  const handleMouseEnter = () => {
    // 预取文章详情
    prefetchData(['post', post.id], () => fetchPostDetail(post.id));
    
    // 预取作者信息
    prefetchData(['user', post.authorId], () => fetchUser(post.authorId));
  };
  
  return (
    <div 
      className="post-card"
      onMouseEnter={handleMouseEnter}
    >
      <h3>{post.title}</h3>
      <p>{post.excerpt}</p>
    </div>
  );
}

缓存持久化

javascript 复制代码
// SWR 缓存持久化
function SWRPersistence() {
  // 本地存储配置
  const localStorageProvider = () => {
    // 在初始化时,我们将数据从 localStorage 恢复到 map 中
    const map = new Map(JSON.parse(localStorage.getItem('app-cache') || '[]'));
    
    // 在卸载应用程序之前,我们将所有数据写回 localStorage
    window.addEventListener('beforeunload', () => {
      const appCache = JSON.stringify(Array.from(map.entries()));
      localStorage.setItem('app-cache', appCache);
    });
    
    return map;
  };
  
  // 选择性持久化
  const persistentSWR = (key, fetcher, options = {}) => {
    return useSWR(key, fetcher, {
      ...options,
      // 只持久化特定数据
      use: [
        (useSWRNext) => (k, f, config) => {
          // 检查是否需要持久化
          if (config.persist) {
            return useSWRNext(k, f, {
              ...config,
              provider: localStorageProvider,
            });
          }
          return useSWRNext(k, f, config);
        }
      ]
    });
  };
  
  return { persistentSWR };
}

// 使用持久化缓存
function UserSettings() {
  const { data: settings } = useSWR(
    'user-settings',
    fetchUserSettings,
    {
      persist: true, // 标记为需要持久化
      revalidateOnMount: false,
    }
  );
  
  return <SettingsPanel settings={settings} />;
}

缓存监控和调试

javascript 复制代码
// SWR 缓存监控
function SWRCacheMonitor() {
  const [cacheEntries, setCacheEntries] = useState([]);
  
  // 监控缓存变化
  useEffect(() => {
    const updateCacheInfo = () => {
      const entries = [];
      for (const [key, value] of cache.entries()) {
        entries.push({
          key: JSON.stringify(key),
          hasData: value.data !== undefined,
          hasError: value.error !== undefined,
          isLoading: value.isLoading,
          lastUpdate: value.lastUpdate,
        });
      }
      setCacheEntries(entries);
    };
    
    // 定期更新缓存信息
    const interval = setInterval(updateCacheInfo, 1000);
    updateCacheInfo();
    
    return () => clearInterval(interval);
  }, []);
  
  // 缓存操作工具
  const clearCache = () => {
    cache.clear();
    setCacheEntries([]);
  };
  
  const invalidateKey = (key) => {
    try {
      const parsedKey = JSON.parse(key);
      mutate(parsedKey);
    } catch (error) {
      console.error('Invalid key format:', error);
    }
  };
  
  return (
    <div className="cache-monitor">
      <div className="monitor-header">
        <h3>SWR 缓存监控</h3>
        <button onClick={clearCache}>清空缓存</button>
      </div>
      
      <div className="cache-stats">
        <p>缓存条目数: {cacheEntries.length}</p>
        <p>内存使用: {(JSON.stringify([...cache.entries()]).length / 1024).toFixed(2)} KB</p>
      </div>
      
      <table className="cache-table">
        <thead>
          <tr>
            <th>缓存键</th>
            <th>状态</th>
            <th>最后更新</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          {cacheEntries.map((entry, index) => (
            <tr key={index}>
              <td className="key-cell">{entry.key}</td>
              <td>
                <span className={`status ${entry.hasError ? 'error' : entry.hasData ? 'success' : 'loading'}`}>
                  {entry.hasError ? '错误' : entry.hasData ? '已缓存' : '加载中'}
                </span>
              </td>
              <td>{entry.lastUpdate ? new Date(entry.lastUpdate).toLocaleTimeString() : '-'}</td>
              <td>
                <button onClick={() => invalidateKey(entry.key)}>
                  重新验证
                </button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

// 缓存性能分析
function useCachePerformance() {
  const [stats, setStats] = useState({
    hits: 0,
    misses: 0,
    totalRequests: 0,
  });
  
  useEffect(() => {
    let hits = 0;
    let misses = 0;
    
    // 监听缓存事件(需要自定义实现)
    const originalFetcher = window.fetch;
    window.fetch = async (...args) => {
      const cacheKey = args[0];
      const hasCachedData = cache.has(cacheKey);
      
      if (hasCachedData) {
        hits++;
      } else {
        misses++;
      }
      
      setStats(prev => ({
        hits: prev.hits + (hasCachedData ? 1 : 0),
        misses: prev.misses + (hasCachedData ? 0 : 1),
        totalRequests: prev.totalRequests + 1,
      }));
      
      return originalFetcher(...args);
    };
    
    return () => {
      window.fetch = originalFetcher;
    };
  }, []);
  
  const hitRate = stats.totalRequests > 0 ? (stats.hits / stats.totalRequests * 100).toFixed(2) : 0;
  
  return {
    ...stats,
    hitRate: `${hitRate}%`,
  };
}

性能对比分析

Bundle 大小对比

javascript 复制代码
// 包大小分析(gzip压缩后)
const bundleAnalysis = {
  'TanStack Query': {
    core: '~39KB',
    devtools: '~45KB',
    total: '~84KB'
  },
  'SWR': {
    core: '~24KB',
    devtools: 'N/A',
    total: '~24KB'
  }
};

// 性能基准测试
class PerformanceBenchmark {
  static async measureCacheHit(library, iterations = 1000) {
    const startTime = performance.now();
    
    for (let i = 0; i < iterations; i++) {
      // 模拟缓存命中
      await library.getFromCache(`test-key-${i % 100}`);
    }
    
    const endTime = performance.now();
    return endTime - startTime;
  }
  
  static async runComparison() {
    const results = {
      tanstackQuery: {
        cacheHit: await this.measureCacheHit(tanstackQuery),
        revalidation: await this.measureRevalidation(tanstackQuery)
      },
      swr: {
        cacheHit: await this.measureCacheHit(swr),
        revalidation: await this.measureRevalidation(swr)
      }
    };
    
    console.table(results);
    return results;
  }
}

内存缓存机制对比

是的,TanStack Query 和 SWR 默认都使用的是内存缓存(Memory Cache),但它们的行为、设计哲学和扩展能力有明显区别。

特性 SWR TanStack Query (React Query)
✅ 是否默认内存缓存 ✅ 是,基于 Map() ✅ 是,使用 QueryCache(内部也是内存)
🧠 缓存结构 Map<key, data> 简单结构 结构复杂,包含 metadata、状态、observer 等
🧮 缓存元信息 ❌ 无元信息,只是简单的缓存值 ✅ 有完整 metadata(状态、时间戳、是否 stale、是否 active)
📦 缓存作用域 全局共享(同一 App 内) 全局共享(但可通过 QueryClient 自定义作用域)
🔄 自动清除机制 ❌ 默认不会清除 ✅ 有 cacheTime,超过自动 GC
⏱ 是否支持过期控制 ❌ 不支持,需手动实现 TTL staleTime / cacheTime 内建
🔧 是否可自定义缓存 ✅ 支持 cacheProvider 替换(如 localStorage) ✅ 支持自定义 QueryCache,但需要更复杂的扩展

两者默认缓存底层是怎么实现的?

🚀 SWR

默认缓存结构:

ts 复制代码
const cache = new Map<string, any>()

你也可以通过 SWRConfig 替换:

tsx 复制代码
<SWRConfig value={{ provider: () => new Map() }}>
  <App />
</SWRConfig>

🚀 TanStack Query

默认缓存是通过 QueryClient 持有的 QueryCache 管理:

ts 复制代码
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      cacheTime: 5 * 60 * 1000, // 默认 5 分钟自动清理
      staleTime: 0              // 默认立即 stale
    }
  }
})

它的缓存结构是:

ts 复制代码
Map<queryHash, QueryInstance>

每个 QueryInstance 包含:

  • data
  • dataUpdatedAt
  • isStale
  • observers(组件)
  • status / fetchStatus
  • retryCount 等 metadata

两者默认都是基于"内存缓存",但 TanStack Query 更复杂更智能,而 SWR 更轻量更灵活。

对比项 SWR TanStack Query
内存缓存结构 简单 key-value 结构化、有状态
生命周期控制 手动 自动(staleTime / cacheTime)
扩展性 非常灵活 高度可配置但复杂
使用场景 轻量级请求、组件驱动 复杂应用、分页、并发请求管理

如你要做缓存持久化、缓存同步跨 Tab、缓存加密等扩展,两者都可以做到,但实现路径略有不同

使用场景选择

选择决策树

graph TD A[选择数据获取库] --> B{项目复杂度} B -->|简单到中等| C{团队经验} B -->|复杂企业级| D[TanStack Query] C -->|React新手| E[SWR] C -->|有经验| F{功能需求} F -->|基础CRUD| E F -->|复杂查询逻辑| D D --> G[丰富的查询功能] D --> H[完善的开发工具] D --> I[企业级特性] E --> J[简单易用] E --> K[轻量级] E --> L[快速上手] style D fill:#e1f5fe style E fill:#f3e5f5

具体使用场景

javascript 复制代码
// 场景1: 简单的数据展示应用 - 推荐 SWR
function SimpleApp() {
  const ProfilePage = () => {
    const { data: user } = useSWR('/api/user', fetcher);
    const { data: posts } = useSWR('/api/posts', fetcher);
    
    return (
      <div>
        <UserCard user={user} />
        <PostsList posts={posts} />
      </div>
    );
  };
  
  return <ProfilePage />;
}

// 场景2: 复杂的企业级应用 - 推荐 TanStack Query
function EnterpriseApp() {
  const DashboardPage = () => {
    // 复杂的并行查询
    const userQuery = useQuery({
      queryKey: ['user'],
      queryFn: fetchUser
    });
    
    const projectsQuery = useQuery({
      queryKey: ['projects', userQuery.data?.id],
      queryFn: () => fetchProjects(userQuery.data.id),
      enabled: !!userQuery.data?.id
    });
    
    // 无限滚动
    const tasksQuery = useInfiniteQuery({
      queryKey: ['tasks'],
      queryFn: fetchTasks,
      getNextPageParam: (lastPage) => lastPage.nextCursor
    });
    
    return (
      <div className="dashboard">
        <UserSection user={userQuery.data} />
        <ProjectsGrid projects={projectsQuery.data} />
        <TasksList tasks={tasksQuery.data?.pages} />
      </div>
    );
  };
  
  return <DashboardPage />;
}

数据状态细化对比

特性 SWR TanStack Query
缓存是否"stale" 自动 + 模糊控制 明确用 staleTime 控制
缓存是否"过期" 默认不过期(除非页面刷新) cacheTime 过期回收
缓存是否清除 页面刷新或手动触发 cacheTime 自动清除
缓存元信息 无(以 value 为主) 有详细状态、时间戳等元信息

最佳实践

TanStack Query 最佳实践

javascript 复制代码
// 1. 查询键管理
const queryKeys = {
  users: {
    all: ['users'] as const,
    lists: () => [...queryKeys.users.all, 'list'] as const,
    list: (filters: UserFilters) => 
      [...queryKeys.users.lists(), { filters }] as const,
    details: () => [...queryKeys.users.all, 'detail'] as const,
    detail: (id: string) => [...queryKeys.users.details(), id] as const,
  },
};

// 2. 自定义Hook封装
function useUser(userId: string) {
  return useQuery({
    queryKey: queryKeys.users.detail(userId),
    queryFn: () => api.get(`/users/${userId}`).then(res => res.data),
    staleTime: 5 * 60 * 1000,
    select: (data) => ({
      ...data,
      fullName: `${data.firstName} ${data.lastName}`
    })
  });
}

SWR 最佳实践

javascript 复制代码
// 1. 自定义Hook模式
function useUser(userId?: string) {
  const { data, error, isLoading, mutate } = useSWR(
    userId ? `/api/users/${userId}` : null,
    {
      revalidateOnMount: true,
      dedupingInterval: 5000
    }
  );

  return {
    user: data,
    isLoading,
    isError: !!error,
    error,
    refresh: mutate
  };
}

// 2. 乐观更新工具
function useOptimisticUpdate() {
  return useCallback(async (
    key: string,
    updateFn: (data: any) => any,
    mutationFn: () => Promise<any>
  ) => {
    try {
      // 获取当前数据
      const currentData = cache.get(key);
      
      // 乐观更新
      const optimisticData = updateFn(currentData);
      mutate(key, optimisticData, false);
      
      // 执行实际请求
      const result = await mutationFn();
      
      // 更新为服务器数据
      mutate(key, result);
      
      return result;
    } catch (error) {
      // 回滚数据
      mutate(key);
      throw error;
    }
  }, []);
}

迁移指南

从 SWR 迁移到 TanStack Query

javascript 复制代码
// SWR 代码
function UserProfile({ userId }) {
  const { data: user, error, mutate } = useSWR(
    `/api/users/${userId}`,
    fetcher,
    { revalidateOnFocus: false }
  );
  
  if (error) return <div>Error loading user</div>;
  if (!user) return <div>Loading...</div>;
  
  return <UserCard user={user} />;
}

// 迁移到 TanStack Query
function UserProfile({ userId }) {
  const { data: user, error, isLoading } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => api.get(`/users/${userId}`).then(res => res.data),
    refetchOnWindowFocus: false
  });
  
  if (error) return <div>Error loading user</div>;
  if (isLoading) return <div>Loading...</div>;
  
  return <UserCard user={user} />;
}

总结

TanStack Query 和 SWR 都是优秀的数据获取库,选择哪个主要取决于项目需求和团队偏好:

选择 TanStack Query 当:

  • 需要复杂的查询功能(无限滚动、并行查询、依赖查询)
  • 需要强大的开发工具和调试能力
  • 项目规模较大,需要企业级特性
  • 团队有足够的学习时间和经验

选择 SWR 当:

  • 项目相对简单,主要是基础的CRUD操作
  • 希望保持较小的包大小
  • 团队更偏向于简单直接的API
  • 快速原型开发或小型项目

无论选择哪个库,都应该:

  1. 建立良好的错误处理机制
  2. 合理配置缓存策略
  3. 实现适当的加载状态处理
  4. 考虑性能优化和监控
  5. 编写全面的测试覆盖
相关推荐
2301_781668615 小时前
前端基础 JS Vue3 Ajax
前端
上单带刀不带妹6 小时前
前端安全问题怎么解决
前端·安全
Fly-ping6 小时前
【前端】JavaScript 的事件循环 (Event Loop)
开发语言·前端·javascript
SunTecTec6 小时前
IDEA 类上方注释 签名
服务器·前端·intellij-idea
在逃的吗喽7 小时前
黑马头条项目详解
前端·javascript·ajax
袁煦丞7 小时前
有Nextcloud家庭共享不求人:cpolar内网穿透实验室第471个成功挑战
前端·程序员·远程工作
小磊哥er7 小时前
【前端工程化】前端项目开发过程中如何做好通知管理?
前端
拾光拾趣录7 小时前
一次“秒开”变成“转菊花”的线上事故
前端
你我约定有三8 小时前
前端笔记:同源策略、跨域问题
前端·笔记
JHCan3338 小时前
一个没有手动加分号引发的bug
前端·javascript·bug