Appearance
SWR快速入门
概述
SWR (Stale-While-Revalidate) 是由Vercel开发的React Hooks数据获取库。它的名字来源于HTTP缓存失效策略stale-while-revalidate。SWR能够先从缓存返回数据(stale),然后发送请求(revalidate),最后得到最新数据,提供快速响应和自动更新的用户体验。
安装和配置
安装
bash
# npm
npm install swr
# yarn
yarn add swr
# pnpm
pnpm add swr基础配置
jsx
import useSWR from 'swr';
// 定义fetcher函数
const fetcher = (url) => fetch(url).then(res => res.json());
function App() {
const { data, error, isLoading } = useSWR('/api/user', fetcher);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Failed to load</div>;
return <div>Hello {data.name}!</div>;
}核心概念
useSWR Hook
jsx
import useSWR from 'swr';
function Profile() {
const fetcher = (url) => fetch(url).then(r => r.json());
const { data, error, isLoading, isValidating, mutate } = useSWR(
'/api/user/123',
fetcher
);
console.log({
data, // 返回的数据
error, // 错误对象
isLoading, // 是否首次加载
isValidating, // 是否正在重新验证
mutate, // 手动触发重新验证的函数
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
<button onClick={() => mutate()}>Refresh</button>
</div>
);
}Key模式
jsx
// 字符串key
useSWR('/api/user', fetcher);
// 数组key(带参数)
useSWR(['/api/user', id], ([url, id]) =>
fetch(`${url}/${id}`).then(r => r.json())
);
// 函数key
const getKey = (pageIndex, previousPageData) => {
if (previousPageData && !previousPageData.length) return null;
return `/api/users?page=${pageIndex}`;
};
useSWR(getKey, fetcher);
// 条件获取(null key不会发起请求)
const shouldFetch = user?.id;
useSWR(shouldFetch ? `/api/user/${user.id}` : null, fetcher);
// 依赖key
function UserProfile({ userId }) {
const { data: user } = useSWR(`/api/user/${userId}`, fetcher);
const { data: posts } = useSWR(
user ? `/api/posts?author=${user.id}` : null,
fetcher
);
return (
<div>
{user && <h1>{user.name}</h1>}
{posts && (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)}
</div>
);
}Fetcher函数
基础Fetcher
jsx
// 简单fetcher
const fetcher = url => fetch(url).then(r => r.json());
// 带错误处理的fetcher
const fetcher = async (url) => {
const res = await fetch(url);
if (!res.ok) {
const error = new Error('An error occurred while fetching the data.');
error.info = await res.json();
error.status = res.status;
throw error;
}
return res.json();
};
// 使用Axios的fetcher
import axios from 'axios';
const fetcher = url => axios.get(url).then(res => res.data);带参数的Fetcher
jsx
// 多参数fetcher
const fetcher = ([url, token]) =>
fetch(url, {
headers: { Authorization: `Bearer ${token}` }
}).then(r => r.json());
function Profile() {
const token = localStorage.getItem('token');
const { data } = useSWR(['/api/user', token], fetcher);
return <div>{data?.name}</div>;
}
// GraphQL fetcher
const graphqlFetcher = (query, variables) =>
fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
}).then(res => res.json());
function UserComponent({ userId }) {
const query = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
const { data } = useSWR(
[query, { id: userId }],
([q, vars]) => graphqlFetcher(q, vars)
);
return <div>{data?.user?.name}</div>;
}全局配置
SWRConfig
jsx
import { SWRConfig } from 'swr';
const fetcher = (url) => fetch(url).then(r => r.json());
function App() {
return (
<SWRConfig
value={{
fetcher,
refreshInterval: 3000, // 每3秒重新验证
revalidateOnFocus: true, // 窗口聚焦时重新验证
revalidateOnReconnect: true, // 重新连接时重新验证
dedupingInterval: 2000, // 2秒内相同请求去重
errorRetryCount: 3, // 错误重试次数
errorRetryInterval: 5000, // 重试间隔
shouldRetryOnError: true, // 错误时是否重试
onError: (error, key) => {
console.error('SWR Error:', error, key);
},
onSuccess: (data, key) => {
console.log('SWR Success:', data, key);
},
}}
>
<Dashboard />
</SWRConfig>
);
}嵌套配置
jsx
function App() {
return (
<SWRConfig value={{ refreshInterval: 3000 }}>
<Dashboard />
{/* 覆盖父级配置 */}
<SWRConfig value={{ refreshInterval: 0, revalidateOnFocus: false }}>
<Profile />
</SWRConfig>
</SWRConfig>
);
}数据更新
自动重新验证
jsx
function AutoRevalidate() {
const { data } = useSWR('/api/stats', fetcher, {
// 窗口聚焦时重新验证
revalidateOnFocus: true,
// 重新连接时重新验证
revalidateOnReconnect: true,
// 定时重新验证(毫秒)
refreshInterval: 5000,
// 只在窗口可见时定时验证
refreshWhenHidden: false,
// 只在在线时定时验证
refreshWhenOffline: false,
});
return <div>Stats: {data?.count}</div>;
}手动重新验证
jsx
function ManualRevalidate() {
const { data, mutate } = useSWR('/api/user', fetcher);
const handleRefresh = () => {
// 重新验证数据
mutate();
};
const handleUpdate = async () => {
// 更新数据并重新验证
await fetch('/api/user', {
method: 'PUT',
body: JSON.stringify({ name: 'New Name' }),
});
mutate(); // 重新获取数据
};
return (
<div>
<p>{data?.name}</p>
<button onClick={handleRefresh}>Refresh</button>
<button onClick={handleUpdate}>Update</button>
</div>
);
}条件获取
依赖数据获取
jsx
function DependentFetching({ userId }) {
// 先获取用户信息
const { data: user } = useSWR(`/api/user/${userId}`, fetcher);
// 基于用户信息获取项目列表
const { data: projects } = useSWR(
user ? `/api/projects?team=${user.teamId}` : null,
fetcher
);
if (!user) return <div>Loading user...</div>;
if (!projects) return <div>Loading projects...</div>;
return (
<div>
<h1>{user.name}'s Projects</h1>
<ul>
{projects.map(project => (
<li key={project.id}>{project.name}</li>
))}
</ul>
</div>
);
}条件启用
jsx
function ConditionalFetching() {
const [shouldFetch, setShouldFetch] = useState(false);
const { data, error } = useSWR(
shouldFetch ? '/api/data' : null,
fetcher
);
return (
<div>
<button onClick={() => setShouldFetch(true)}>
Load Data
</button>
{data && <div>{JSON.stringify(data)}</div>}
{error && <div>Error loading data</div>}
</div>
);
}分页
基础分页
jsx
function Pagination() {
const [pageIndex, setPageIndex] = useState(0);
const { data, error, isLoading } = useSWR(
`/api/users?page=${pageIndex}&limit=10`,
fetcher
);
return (
<div>
{isLoading && <div>Loading...</div>}
{error && <div>Error</div>}
{data && (
<>
<ul>
{data.users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
<div>
<button
disabled={pageIndex === 0}
onClick={() => setPageIndex(pageIndex - 1)}
>
Previous
</button>
<span>Page {pageIndex + 1}</span>
<button
disabled={!data.hasMore}
onClick={() => setPageIndex(pageIndex + 1)}
>
Next
</button>
</div>
</>
)}
</div>
);
}useSWRInfinite
jsx
import useSWRInfinite from 'swr/infinite';
function InfiniteList() {
const getKey = (pageIndex, previousPageData) => {
// 到达末尾
if (previousPageData && !previousPageData.length) return null;
// SWR key
return `/api/users?page=${pageIndex}&limit=10`;
};
const {
data,
error,
size,
setSize,
isLoading,
isValidating,
} = useSWRInfinite(getKey, fetcher);
const users = data ? data.flat() : [];
const isLoadingMore = isValidating && data && typeof data[size - 1] !== 'undefined';
const isEmpty = data?.[0]?.length === 0;
const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < 10);
return (
<div>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
{error && <div>Error loading data</div>}
<button
disabled={isLoadingMore || isReachingEnd}
onClick={() => setSize(size + 1)}
>
{isLoadingMore
? 'Loading...'
: isReachingEnd
? 'No More Data'
: 'Load More'}
</button>
</div>
);
}预加载
预加载数据
jsx
import { mutate } from 'swr';
function preload(key, fetcher) {
mutate(key, fetcher(key), false);
}
function UserList() {
const { data: users } = useSWR('/api/users', fetcher);
const handleMouseEnter = (userId) => {
// 鼠标悬停时预加载用户详情
preload(`/api/user/${userId}`, fetcher);
};
return (
<ul>
{users?.map(user => (
<li
key={user.id}
onMouseEnter={() => handleMouseEnter(user.id)}
>
<Link to={`/user/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
);
}
function UserDetail({ userId }) {
// 数据可能已被预加载
const { data: user } = useSWR(`/api/user/${userId}`, fetcher);
return <div>{user?.name}</div>;
}路由预加载
jsx
import { useEffect } from 'react';
import { useRouter } from 'next/router';
function LinkWithPreload({ href, children }) {
const router = useRouter();
const handleMouseEnter = () => {
// 预加载路由和数据
router.prefetch(href);
preload(`/api${href}`, fetcher);
};
return (
<Link href={href} onMouseEnter={handleMouseEnter}>
{children}
</Link>
);
}离线支持
离线缓存
jsx
import useSWR from 'swr';
function OfflineSupport() {
const { data, error } = useSWR('/api/user', fetcher, {
// 重新连接时重新验证
revalidateOnReconnect: true,
// 离线时使用缓存
revalidateIfStale: navigator.onLine,
// 离线时不轮询
refreshInterval: navigator.onLine ? 3000 : 0,
// 错误重试
shouldRetryOnError: navigator.onLine,
});
const isOffline = !navigator.onLine;
return (
<div>
{isOffline && (
<div className="offline-banner">
You are offline. Showing cached data.
</div>
)}
{error && <div>Error: {error.message}</div>}
{data && <div>{data.name}</div>}
</div>
);
}监听网络状态
jsx
function NetworkAwareComponent() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
const { data } = useSWR('/api/data', fetcher, {
revalidateOnReconnect: true,
refreshInterval: isOnline ? 3000 : 0,
});
return (
<div>
<div className={isOnline ? 'online' : 'offline'}>
{isOnline ? 'Online' : 'Offline'}
</div>
{data && <div>{JSON.stringify(data)}</div>}
</div>
);
}错误处理
基础错误处理
jsx
function ErrorHandling() {
const { data, error, isLoading } = useSWR('/api/user', fetcher, {
onError: (error, key) => {
console.error(`Error fetching ${key}:`, error);
// 发送到错误追踪服务
logErrorToService(error, key);
},
// 错误重试
errorRetryCount: 3,
errorRetryInterval: 5000,
// 自定义重试条件
shouldRetryOnError: (error) => {
// 只重试5xx错误
return error.status >= 500;
},
});
if (isLoading) return <div>Loading...</div>;
if (error) {
return (
<div className="error">
<h3>Error</h3>
<p>{error.message}</p>
{error.status && <p>Status: {error.status}</p>}
</div>
);
}
return <div>{data.name}</div>;
}错误边界集成
jsx
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<SWRConfig value={{ fetcher }}>
<Dashboard />
</SWRConfig>
</ErrorBoundary>
);
}总结
SWR核心特性:
- Hooks API:简洁的useSWR Hook
- 自动重新验证:聚焦、重连、定时验证
- 缓存策略:Stale-While-Revalidate
- 全局配置:SWRConfig统一设置
- 条件获取:依赖数据、条件启用
- 分页支持:基础分页、无限滚动
- 预加载:数据预加载、路由预加载
- 离线支持:缓存使用、网络感知
- 错误处理:重试机制、错误边界
SWR提供了优雅的数据获取方案,特别适合需要实时数据和快速响应的应用。
第四部分:SWR高级应用
4.1 性能优化技巧
jsx
// 1. 使用useSWRImmutable避免重新验证
import useSWRImmutable from 'swr/immutable';
function StaticData() {
// 数据不会改变,不需要重新验证
const { data } = useSWRImmutable('/api/static-config', fetcher);
return <div>{JSON.stringify(data)}</div>;
}
// 2. 使用useSWRInfinite优化无限滚动
import useSWRInfinite from 'swr/infinite';
function OptimizedInfiniteList() {
const getKey = (pageIndex, previousPageData) => {
if (previousPageData && !previousPageData.hasMore) return null;
return `/api/items?page=${pageIndex}&limit=20`;
};
const { data, size, setSize, isValidating } = useSWRInfinite(getKey, fetcher, {
revalidateFirstPage: false, // 不重新验证第一页
persistSize: true // 保持size状态
});
const items = data ? data.flatMap(page => page.items) : [];
const isLoadingMore = isValidating && data && typeof data[size - 1] !== 'undefined';
const isEmpty = data?.[0]?.items.length === 0;
const isReachingEnd = isEmpty || (data && !data[data.length - 1]?.hasMore);
return (
<div>
{items.map(item => (
<div key={item.id}>{item.name}</div>
))}
{!isReachingEnd && (
<button
onClick={() => setSize(size + 1)}
disabled={isLoadingMore}
>
{isLoadingMore ? '加载中...' : '加载更多'}
</button>
)}
</div>
);
}
// 3. 依赖数据优化
function DependentData() {
const { data: user } = useSWR('/api/user', fetcher);
// 只在user存在时才获取
const { data: profile } = useSWR(
user ? `/api/users/${user.id}/profile` : null,
fetcher
);
// 使用fallbackData避免闪烁
const { data: settings } = useSWR(
user ? `/api/users/${user.id}/settings` : null,
fetcher,
{
fallbackData: { theme: 'light', language: 'zh' }
}
);
return <div>...</div>;
}
// 4. 并行请求优化
function ParallelRequests() {
const { data: user } = useSWR('/api/user', fetcher);
const { data: posts } = useSWR('/api/posts', fetcher);
const { data: comments } = useSWR('/api/comments', fetcher);
// 使用Promise.all预加载
useEffect(() => {
Promise.all([
fetcher('/api/user'),
fetcher('/api/posts'),
fetcher('/api/comments')
]).then(([userData, postsData, commentsData]) => {
// 数据预填充到缓存
mutate('/api/user', userData, false);
mutate('/api/posts', postsData, false);
mutate('/api/comments', commentsData, false);
});
}, []);
return <div>...</div>;
}
// 5. 智能预加载
function SmartPrefetch() {
const [hoveredId, setHoveredId] = useState(null);
// 鼠标悬停时预加载
const handleMouseEnter = (id) => {
setHoveredId(id);
mutate(`/api/items/${id}`, fetcher(`/api/items/${id}`), false);
};
return (
<div>
{items.map(item => (
<div
key={item.id}
onMouseEnter={() => handleMouseEnter(item.id)}
>
{item.name}
</div>
))}
</div>
);
}4.2 数据同步策略
jsx
// 1. 跨标签页同步
import { useSWRConfig } from 'swr';
function CrossTabSync() {
const { mutate } = useSWRConfig();
useEffect(() => {
// 监听storage事件实现跨标签页同步
const handleStorage = (e) => {
if (e.key?.startsWith('swr-')) {
const key = e.key.replace('swr-', '');
mutate(key);
}
};
window.addEventListener('storage', handleStorage);
return () => window.removeEventListener('storage', handleStorage);
}, [mutate]);
return <div>跨标签页数据同步</div>;
}
// 2. WebSocket实时更新
function RealtimeSync() {
const { data, mutate } = useSWR('/api/messages', fetcher);
useEffect(() => {
const ws = new WebSocket('ws://localhost:3000');
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
// 乐观更新
mutate(
(currentData) => [...(currentData || []), message],
{ revalidate: false }
);
};
return () => ws.close();
}, [mutate]);
return (
<div>
{data?.map(msg => (
<div key={msg.id}>{msg.content}</div>
))}
</div>
);
}
// 3. 轮询优化
function OptimizedPolling() {
const [isActive, setIsActive] = useState(true);
const { data } = useSWR(
'/api/status',
fetcher,
{
refreshInterval: isActive ? 1000 : 0, // 根据状态调整轮询
refreshWhenHidden: false, // 隐藏时停止轮询
refreshWhenOffline: false, // 离线时停止轮询
onSuccess: (data) => {
// 根据数据决定是否继续轮询
if (data.status === 'completed') {
setIsActive(false);
}
}
}
);
return <div>状态: {data?.status}</div>;
}
// 4. 增量更新
function IncrementalUpdate() {
const { data, mutate } = useSWR('/api/feed', fetcher);
const addItem = async (newItem) => {
// 乐观添加
mutate(
(currentData) => [newItem, ...(currentData || [])],
false
);
try {
await fetch('/api/items', {
method: 'POST',
body: JSON.stringify(newItem)
});
// 成功后重新验证
mutate();
} catch (error) {
// 失败回滚
mutate();
}
};
return <div>...</div>;
}4.3 缓存管理策略
jsx
// 1. 自定义缓存提供者
import { SWRConfig } from 'swr';
function createCustomCache() {
const cache = new Map();
const listeners = new Set();
return {
get(key) {
return cache.get(key);
},
set(key, value) {
cache.set(key, value);
listeners.forEach(listener => listener());
},
delete(key) {
cache.delete(key);
},
keys() {
return Array.from(cache.keys());
},
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
};
}
function AppWithCustomCache() {
const cache = useMemo(() => createCustomCache(), []);
return (
<SWRConfig value={{ provider: () => cache }}>
<App />
</SWRConfig>
);
}
// 2. 持久化缓存
function PersistentCache() {
const cache = useMemo(() => {
const map = new Map();
// 从localStorage恢复
try {
const saved = localStorage.getItem('swr-cache');
if (saved) {
const entries = JSON.parse(saved);
entries.forEach(([key, value]) => map.set(key, value));
}
} catch (e) {
console.error('Failed to restore cache', e);
}
// 定期保存
const saveInterval = setInterval(() => {
try {
const entries = Array.from(map.entries());
localStorage.setItem('swr-cache', JSON.stringify(entries));
} catch (e) {
console.error('Failed to save cache', e);
}
}, 5000);
return {
...map,
cleanup: () => clearInterval(saveInterval)
};
}, []);
useEffect(() => {
return () => cache.cleanup?.();
}, [cache]);
return (
<SWRConfig value={{ provider: () => cache }}>
<App />
</SWRConfig>
);
}
// 3. LRU缓存
class LRUCache {
constructor(maxSize = 100) {
this.maxSize = maxSize;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return undefined;
// 移到最后(最近使用)
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
set(key, value) {
// 如果存在,先删除
if (this.cache.has(key)) {
this.cache.delete(key);
}
// 添加到最后
this.cache.set(key, value);
// 超出容量,删除最旧的
if (this.cache.size > this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
}
delete(key) {
this.cache.delete(key);
}
keys() {
return Array.from(this.cache.keys());
}
}
// 使用LRU缓存
function AppWithLRUCache() {
const cache = useMemo(() => new LRUCache(50), []);
return (
<SWRConfig value={{ provider: () => cache }}>
<App />
</SWRConfig>
);
}
// 4. 缓存过期策略
function CacheWithExpiry() {
const cache = useMemo(() => {
const data = new Map();
const expiry = new Map();
return {
get(key) {
const expiryTime = expiry.get(key);
if (expiryTime && Date.now() > expiryTime) {
data.delete(key);
expiry.delete(key);
return undefined;
}
return data.get(key);
},
set(key, value, ttl = 60000) {
data.set(key, value);
expiry.set(key, Date.now() + ttl);
},
delete(key) {
data.delete(key);
expiry.delete(key);
},
keys() {
// 清理过期数据
expiry.forEach((time, key) => {
if (Date.now() > time) {
data.delete(key);
expiry.delete(key);
}
});
return Array.from(data.keys());
}
};
}, []);
return (
<SWRConfig value={{ provider: () => cache }}>
<App />
</SWRConfig>
);
}4.4 错误处理增强
jsx
// 1. 错误重试策略
function SmartRetry() {
const { data, error } = useSWR('/api/data', fetcher, {
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
// 404不重试
if (error.status === 404) return;
// 最多重试3次
if (retryCount >= 3) return;
// 指数退避
const delay = Math.min(1000 * Math.pow(2, retryCount), 30000);
setTimeout(() => revalidate({ retryCount }), delay);
}
});
return <div>...</div>;
}
// 2. 错误边界集成
class SWRErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return (
<div className="error-container">
<h2>数据加载失败</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
重试
</button>
</div>
);
}
return this.props.children;
}
}
// 3. 全局错误处理
function AppWithGlobalErrorHandler() {
const handleError = useCallback((error, key) => {
console.error('SWR Error:', error, 'Key:', key);
// 发送到错误监控
if (window.Sentry) {
window.Sentry.captureException(error, {
contexts: { swr: { key } }
});
}
// 显示用户友好的错误消息
toast.error(`数据加载失败: ${error.message}`);
}, []);
return (
<SWRConfig value={{ onError: handleError }}>
<App />
</SWRConfig>
);
}
// 4. 降级策略
function FallbackStrategy() {
const { data, error } = useSWR('/api/primary', fetcher, {
onError: async (err, key, config) => {
// 主接口失败,尝试备用接口
if (err.status >= 500) {
try {
const fallbackData = await fetcher('/api/fallback');
mutate(key, fallbackData, false);
} catch (fallbackError) {
console.error('Fallback also failed', fallbackError);
}
}
}
});
return <div>{data?.content || '加载中...'}</div>;
}4.5 测试策略
jsx
// 1. Mock SWR
import { SWRConfig } from 'swr';
import { render, screen } from '@testing-library/react';
function TestWrapper({ children, mockData }) {
const mockCache = new Map();
// 预填充mock数据
Object.entries(mockData).forEach(([key, value]) => {
mockCache.set(key, value);
});
return (
<SWRConfig value={{
provider: () => mockCache,
dedupingInterval: 0
}}>
{children}
</SWRConfig>
);
}
// 使用
test('renders user data', () => {
const mockData = {
'/api/user': { name: 'Alice', email: 'alice@example.com' }
};
render(
<TestWrapper mockData={mockData}>
<UserProfile />
</TestWrapper>
);
expect(screen.getByText('Alice')).toBeInTheDocument();
});
// 2. 测试加载状态
test('shows loading state', async () => {
let resolvePromise;
const promise = new Promise(resolve => {
resolvePromise = resolve;
});
const fetcher = () => promise;
render(
<SWRConfig value={{ fetcher }}>
<DataComponent />
</SWRConfig>
);
expect(screen.getByText('加载中...')).toBeInTheDocument();
resolvePromise({ data: 'test' });
await waitFor(() => {
expect(screen.getByText('test')).toBeInTheDocument();
});
});
// 3. 测试错误处理
test('handles errors correctly', async () => {
const fetcher = jest.fn().mockRejectedValue(new Error('Failed'));
render(
<SWRConfig value={{ fetcher }}>
<DataComponent />
</SWRConfig>
);
await waitFor(() => {
expect(screen.getByText('加载失败')).toBeInTheDocument();
});
});
// 4. 测试乐观更新
test('optimistic update works', async () => {
const { result } = renderHook(() => useSWR('/api/todo', fetcher));
await waitFor(() => expect(result.current.data).toBeDefined());
act(() => {
result.current.mutate({ completed: true }, false);
});
expect(result.current.data.completed).toBe(true);
});4.6 实战最佳实践
jsx
// 综合实战案例
function ComprehensiveSWRApp() {
return (
<SWRConfig value={{
// 全局配置
fetcher: (url) => fetch(url).then(r => r.json()),
revalidateOnFocus: true,
revalidateOnReconnect: true,
dedupingInterval: 2000,
// 性能优化
suspense: false,
loadingTimeout: 3000,
// 错误处理
onError: (error, key) => {
console.error('SWR Error:', error, key);
if (error.status !== 403 && error.status !== 404) {
// 发送错误到监控
errorReporting.notify(error);
}
},
// 自定义缓存
provider: () => new LRUCache(100),
// 重试策略
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
if (error.status === 404) return;
if (retryCount >= 3) return;
setTimeout(() => revalidate({ retryCount }),
Math.min(1000 * Math.pow(2, retryCount), 30000)
);
}
}}>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Router>
</SWRConfig>
);
}SWR完整最佳实践
核心要点
1. 性能优化
✅ useSWRImmutable静态数据
✅ 智能预加载
✅ 并行请求优化
✅ 依赖数据处理
2. 数据同步
✅ 跨标签页同步
✅ WebSocket实时更新
✅ 轮询优化
✅ 增量更新
3. 缓存管理
✅ 自定义缓存提供者
✅ 持久化缓存
✅ LRU策略
✅ 过期控制
4. 错误处理
✅ 智能重试
✅ 错误边界
✅ 全局处理
✅ 降级策略
5. 测试
✅ Mock数据
✅ 加载状态测试
✅ 错误处理测试
✅ 乐观更新测试SWR凭借其简洁的API和强大的功能,是React数据获取的优秀选择。