Appearance
Suspense数据获取
第一部分:数据获取基础
1.1 传统数据获取方式
javascript
// 方式1:useEffect + loading状态
function TraditionalFetch() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch('/api/data')
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, []);
if (loading) return <Spinner />;
if (error) return <Error message={error.message} />;
if (!data) return null;
return <DataDisplay data={data} />;
}
// 方式2:自定义Hook
function useData(url) {
const [state, setState] = useState({
data: null,
loading: true,
error: null
});
useEffect(() => {
setState(s => ({ ...s, loading: true }));
fetch(url)
.then(res => res.json())
.then(data => setState({ data, loading: false, error: null }))
.catch(error => setState({ data: null, loading: false, error }));
}, [url]);
return state;
}
// 传统方式的问题:
// 1. 样板代码多
// 2. 手动管理loading/error状态
// 3. 瀑布式加载
// 4. 难以组合1.2 Suspense数据获取模式
javascript
// 使用Suspense的数据获取
function SuspenseFetch() {
const data = use(fetchData()); // React 19的use Hook
return <DataDisplay data={data} />;
}
// 使用
function App() {
return (
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<Spinner />}>
<SuspenseFetch />
</Suspense>
</ErrorBoundary>
);
}
// 优势:
// 1. 代码简洁
// 2. 声明式加载状态
// 3. 并行加载
// 4. 易于组合1.3 use Hook详解
javascript
// React 19的use Hook
import { use } from 'react';
// 1. 读取Promise
function Component() {
const data = use(fetchPromise); // 如果pending则挂起
return <div>{data}</div>;
}
// 2. 读取Context
function Component() {
const theme = use(ThemeContext);
return <div className={theme}></div>;
}
// 3. 条件使用(与其他Hook不同)
function Component({ shouldFetch }) {
if (shouldFetch) {
const data = use(fetchData()); // ✅ 可以在条件中
return <div>{data}</div>;
}
return <div>No data</div>;
}
// 4. 循环中使用
function Component({ ids }) {
return ids.map(id => {
const data = use(fetchItem(id)); // ✅ 可以在循环中
return <Item key={id} data={data} />;
});
}1.4 创建Suspense数据源
javascript
// 基础实现
function fetchData(url) {
let status = 'pending';
let result;
const promise = fetch(url)
.then(res => res.json())
.then(data => {
status = 'success';
result = data;
})
.catch(error => {
status = 'error';
result = error;
});
return {
read() {
if (status === 'pending') {
throw promise; // Suspense挂起
} else if (status === 'error') {
throw result; // ErrorBoundary捕获
} else {
return result; // 返回数据
}
}
};
}
// 使用
function Component() {
const resource = fetchData('/api/user');
const data = resource.read();
return <div>{data.name}</div>;
}
// 改进:支持缓存
const cache = new Map();
function createResource(fetchFunc) {
return {
read(key) {
if (cache.has(key)) {
const cached = cache.get(key);
if (cached.status === 'pending') {
throw cached.promise;
} else if (cached.status === 'error') {
throw cached.error;
} else {
return cached.data;
}
}
const promise = fetchFunc(key)
.then(data => {
cache.set(key, { status: 'success', data });
return data;
})
.catch(error => {
cache.set(key, { status: 'error', error });
throw error;
});
cache.set(key, { status: 'pending', promise });
throw promise;
}
};
}
// 使用带缓存的resource
const userResource = createResource(fetchUser);
function UserProfile({ userId }) {
const user = userResource.read(userId);
return <div>{user.name}</div>;
}第二部分:实战模式
2.1 基础数据获取
javascript
// 简单GET请求
function fetchUser(id) {
return fetch(`/api/users/${id}`)
.then(res => {
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
});
}
function UserComponent({ userId }) {
const user = use(fetchUser(userId));
return (
<div className="user-card">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.bio}</p>
</div>
);
}
// 使用
function App() {
return (
<Suspense fallback={<UserSkeleton />}>
<UserComponent userId={1} />
</Suspense>
);
}
// POST请求
async function createUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
if (!response.ok) throw new Error('Failed to create user');
return response.json();
}
function CreateUserForm() {
const [userData, setUserData] = useState(null);
const handleSubmit = (formData) => {
setUserData(formData);
};
if (!userData) {
return <Form onSubmit={handleSubmit} />;
}
return (
<Suspense fallback={<Saving />}>
<SubmitUser data={userData} />
</Suspense>
);
}
function SubmitUser({ data }) {
const result = use(createUser(data));
return <SuccessMessage user={result} />;
}2.2 并行数据获取
javascript
// 传统方式:串行加载(瀑布式)
function WaterfallLoading({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);
useEffect(() => {
fetchUser(userId).then(userData => {
setUser(userData);
// 等user加载完才加载posts
fetchPosts(userId).then(setPosts);
});
}, [userId]);
if (!user) return <Loading />;
if (!posts) return <Loading />;
return <ProfilePage user={user} posts={posts} />;
}
// Suspense方式:并行加载
function ParallelLoading({ userId }) {
const user = use(fetchUser(userId));
const posts = use(fetchPosts(userId));
// 两个请求同时发起,都完成后才渲染
return <ProfilePage user={user} posts={posts} />;
}
// 使用
function App({ userId }) {
return (
<Suspense fallback={<ProfileSkeleton />}>
<ParallelLoading userId={userId} />
</Suspense>
);
}
// 更复杂的并行加载
function Dashboard({ userId }) {
const user = use(fetchUser(userId));
const stats = use(fetchStats(userId));
const activities = use(fetchActivities(userId));
const notifications = use(fetchNotifications(userId));
// 4个请求并行,全部完成后渲染
return (
<div className="dashboard">
<UserHeader user={user} />
<StatsPanel stats={stats} />
<ActivityFeed activities={activities} />
<NotificationList notifications={notifications} />
</div>
);
}2.3 嵌套数据获取
javascript
// 逐步加载(瀑布优化)
function ProfilePage({ userId }) {
return (
<div className="profile">
{/* 第一层:快速加载用户基本信息 */}
<Suspense fallback={<HeaderSkeleton />}>
<ProfileHeader userId={userId} />
</Suspense>
{/* 第二层:加载用户详情 */}
<Suspense fallback={<ContentSkeleton />}>
<ProfileContent userId={userId} />
{/* 第三层:加载评论 */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments userId={userId} />
</Suspense>
</Suspense>
</div>
);
}
function ProfileHeader({ userId }) {
const user = use(fetchUserBasic(userId));
return (
<header>
<img src={user.avatar} alt={user.name} />
<h1>{user.name}</h1>
</header>
);
}
function ProfileContent({ userId }) {
const details = use(fetchUserDetails(userId));
return (
<div className="content">
<Bio text={details.bio} />
<Stats data={details.stats} />
</div>
);
}
function Comments({ userId }) {
const comments = use(fetchComments(userId));
return (
<div className="comments">
{comments.map(comment => (
<Comment key={comment.id} data={comment} />
))}
</div>
);
}
// 结果:
// 1. 用户头像、名字立即显示
// 2. 详情信息稍后显示
// 3. 评论最后显示
// 每一层都不阻塞其他层的显示2.4 条件数据获取
javascript
// 根据条件获取数据
function ConditionalData({ showDetails, userId }) {
const basicInfo = use(fetchUserBasic(userId));
let detailInfo = null;
if (showDetails) {
detailInfo = use(fetchUserDetails(userId)); // ✅ use可以在条件中
}
return (
<div>
<h1>{basicInfo.name}</h1>
{detailInfo && (
<div className="details">
<p>{detailInfo.bio}</p>
<Stats data={detailInfo.stats} />
</div>
)}
</div>
);
}
// 使用
function App() {
const [showDetails, setShowDetails] = useState(false);
return (
<div>
<button onClick={() => setShowDetails(!showDetails)}>
{showDetails ? '隐藏' : '显示'}详情
</button>
<Suspense fallback={<Loading />}>
<ConditionalData showDetails={showDetails} userId={1} />
</Suspense>
</div>
);
}
// 权限控制
function ProtectedData({ userId, hasPermission }) {
const publicData = use(fetchPublicData(userId));
if (!hasPermission) {
return <PublicView data={publicData} />;
}
const privateData = use(fetchPrivateData(userId));
return <PrivateView public={publicData} private={privateData} />;
}2.5 依赖数据获取
javascript
// 数据依赖关系
function DependentData({ categoryId }) {
const category = use(fetchCategory(categoryId));
// 基于category的结果获取products
const products = use(fetchProductsByCategory(category.id));
return (
<div>
<h2>{category.name}</h2>
<ProductList products={products} />
</div>
);
}
// 多级依赖
function MultiLevelDependent({ userId }) {
// 第一级
const user = use(fetchUser(userId));
// 第二级:依赖user
const preferences = use(fetchPreferences(user.preferenceId));
// 第三级:依赖preferences
const recommendations = use(
fetchRecommendations(preferences.categories)
);
return (
<div>
<h1>{user.name}</h1>
<Preferences data={preferences} />
<Recommendations items={recommendations} />
</div>
);
}
// 优化:尽可能并行
function OptimizedDependent({ userId }) {
const user = use(fetchUser(userId));
// 这两个不相互依赖,可以并行
const preferences = use(fetchPreferences(user.preferenceId));
const history = use(fetchHistory(user.id));
return (
<div>
<h1>{user.name}</h1>
<div className="parallel-content">
<Preferences data={preferences} />
<History data={history} />
</div>
</div>
);
}第三部分:高级技巧
3.1 数据预加载
javascript
// 预加载策略
const preloadedData = new Map();
function preload(key, fetcher) {
if (!preloadedData.has(key)) {
const promise = fetcher();
preloadedData.set(key, promise);
}
return preloadedData.get(key);
}
// 链接预加载
function UserLink({ userId, children }) {
const handleMouseEnter = () => {
preload(`user-${userId}`, () => fetchUser(userId));
};
return (
<Link
to={`/users/${userId}`}
onMouseEnter={handleMouseEnter}
>
{children}
</Link>
);
}
// 路由预加载
function RoutePreload() {
const navigate = useNavigate();
const goToUser = (userId) => {
// 预加载数据
preload(`user-${userId}`, () => fetchUser(userId));
preload(`posts-${userId}`, () => fetchPosts(userId));
// 然后导航
navigate(`/users/${userId}`);
};
return (
<button onClick={() => goToUser(1)}>
查看用户
</button>
);
}
// 智能预加载
function SmartPreload() {
const [currentId, setCurrentId] = useState(1);
useEffect(() => {
// 预加载相邻数据
const nextId = currentId + 1;
const prevId = currentId - 1;
if (nextId <= 10) {
preload(`user-${nextId}`, () => fetchUser(nextId));
}
if (prevId >= 1) {
preload(`user-${prevId}`, () => fetchUser(prevId));
}
}, [currentId]);
return (
<div>
<button onClick={() => setCurrentId(id => id - 1)}>
上一个
</button>
<Suspense fallback={<Skeleton />}>
<UserProfile userId={currentId} />
</Suspense>
<button onClick={() => setCurrentId(id => id + 1)}>
下一个
</button>
</div>
);
}3.2 缓存管理
javascript
// 带过期时间的缓存
class DataCache {
constructor(ttl = 60000) { // 默认1分钟
this.cache = new Map();
this.ttl = ttl;
}
set(key, value) {
this.cache.set(key, {
value,
timestamp: Date.now()
});
}
get(key) {
const cached = this.cache.get(key);
if (!cached) return null;
const age = Date.now() - cached.timestamp;
if (age > this.ttl) {
this.cache.delete(key);
return null;
}
return cached.value;
}
clear() {
this.cache.clear();
}
invalidate(key) {
this.cache.delete(key);
}
}
const dataCache = new DataCache(5 * 60 * 1000); // 5分钟
function fetchWithCache(url) {
const cached = dataCache.get(url);
if (cached) {
return Promise.resolve(cached);
}
return fetch(url)
.then(res => res.json())
.then(data => {
dataCache.set(url, data);
return data;
});
}
// SWR模式(Stale-While-Revalidate)
function useSWR(key, fetcher) {
const cached = dataCache.get(key);
if (cached) {
// 返回缓存,同时后台刷新
fetcher().then(fresh => {
dataCache.set(key, fresh);
});
return cached;
}
// 无缓存,挂起
const promise = fetcher().then(data => {
dataCache.set(key, data);
return data;
});
throw promise;
}
// 使用SWR
function UserProfile({ userId }) {
const user = useSWR(
`user-${userId}`,
() => fetchUser(userId)
);
return <div>{user.name}</div>;
}
// 条件刷新
function ConditionalRevalidate({ userId, shouldRevalidate }) {
useEffect(() => {
if (shouldRevalidate) {
dataCache.invalidate(`user-${userId}`);
}
}, [userId, shouldRevalidate]);
return (
<Suspense fallback={<Loading />}>
<UserProfile userId={userId} />
</Suspense>
);
}3.3 错误重试
javascript
// 带重试的数据获取
function fetchWithRetry(url, maxRetries = 3) {
let retries = 0;
const attempt = async () => {
try {
const res = await fetch(url);
if (!res.ok) throw new Error('Request failed');
return await res.json();
} catch (error) {
retries++;
if (retries >= maxRetries) {
throw error;
}
// 指数退避
const delay = Math.pow(2, retries) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
return attempt();
}
};
return attempt();
}
// 使用
function RobustData({ userId }) {
const user = use(fetchWithRetry(`/api/users/${userId}`, 3));
return <div>{user.name}</div>;
}
// 带UI反馈的重试
function RetryableComponent() {
const [retryKey, setRetryKey] = useState(0);
return (
<ErrorBoundary
fallback={(error) => (
<div className="error">
<p>加载失败: {error.message}</p>
<button onClick={() => setRetryKey(k => k + 1)}>
重试
</button>
</div>
)}
resetKeys={[retryKey]}
>
<Suspense fallback={<Loading />}>
<DataComponent key={retryKey} />
</Suspense>
</ErrorBoundary>
);
}3.4 分页加载
javascript
// 分页数据获取
function PaginatedData({ page }) {
const data = use(fetchPage(page));
return (
<div>
{data.items.map(item => (
<Item key={item.id} data={item} />
))}
</div>
);
}
// 使用
function PaginatedList() {
const [page, setPage] = useState(1);
return (
<div>
<Suspense fallback={<ListSkeleton />}>
<PaginatedData page={page} />
</Suspense>
<div className="pagination">
<button
onClick={() => setPage(p => p - 1)}
disabled={page === 1}
>
上一页
</button>
<span>第 {page} 页</span>
<button onClick={() => setPage(p => p + 1)}>
下一页
</button>
</div>
</div>
);
}
// 无限滚动
function InfiniteScroll() {
const [pages, setPages] = useState([1]);
const observerRef = useRef();
const loadMore = () => {
setPages(p => [...p, p.length + 1]);
};
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
loadMore();
}
},
{ threshold: 0.1 }
);
if (observerRef.current) {
observer.observe(observerRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div>
{pages.map(page => (
<Suspense key={page} fallback={<PageSkeleton />}>
<PageData page={page} />
</Suspense>
))}
<div ref={observerRef} />
</div>
);
}3.5 实时更新
javascript
// WebSocket + Suspense
function useRealtimeData(url, initialFetch) {
const [data, setData] = useState(() => {
throw initialFetch().then(setData);
});
useEffect(() => {
const ws = new WebSocket(url);
ws.onmessage = (event) => {
const update = JSON.parse(event.data);
setData(update);
};
return () => ws.close();
}, [url]);
return data;
}
// 使用
function LiveData() {
const data = useRealtimeData(
'ws://api.example.com/live',
() => fetch('/api/initial').then(r => r.json())
);
return <DataDisplay data={data} />;
}
// Server-Sent Events
function useSSE(url, initialFetch) {
const [data, setData] = useState(() => {
throw initialFetch().then(setData);
});
useEffect(() => {
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
const update = JSON.parse(event.data);
setData(update);
};
return () => eventSource.close();
}, [url]);
return data;
}第四部分:数据获取库集成
4.1 使用React Query
javascript
// React Query + Suspense
import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true // 启用Suspense模式
}
}
});
function UserProfile({ userId }) {
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
// 在Suspense模式下,data总是有值
return <div>{data.name}</div>;
}
// 使用
function App() {
return (
<QueryClientProvider client={queryClient}>
<Suspense fallback={<Loading />}>
<UserProfile userId={1} />
</Suspense>
</QueryClientProvider>
);
}
// 预取数据
function UserLink({ userId }) {
const queryClient = useQueryClient();
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
};
return (
<Link
to={`/users/${userId}`}
onMouseEnter={handleMouseEnter}
>
用户 {userId}
</Link>
);
}4.2 使用SWR
javascript
// SWR + Suspense
import useSWR from 'swr';
const fetcher = url => fetch(url).then(r => r.json());
function UserProfile({ userId }) {
const { data } = useSWR(
`/api/users/${userId}`,
fetcher,
{ suspense: true }
);
return <div>{data.name}</div>;
}
// 使用
function App() {
return (
<SWRConfig value={{ suspense: true }}>
<Suspense fallback={<Loading />}>
<UserProfile userId={1} />
</Suspense>
</SWRConfig>
);
}
// 预加载
import { preload } from 'swr';
function UserLink({ userId }) {
const handleMouseEnter = () => {
preload(`/api/users/${userId}`, fetcher);
};
return (
<Link
to={`/users/${userId}`}
onMouseEnter={handleMouseEnter}
>
用户 {userId}
</Link>
);
}4.3 使用Relay
javascript
// Relay + Suspense(原生支持)
import { useLazyLoadQuery } from 'react-relay';
import graphql from 'babel-plugin-relay/macro';
function UserProfile({ userId }) {
const data = useLazyLoadQuery(
graphql`
query UserProfileQuery($userId: ID!) {
user(id: $userId) {
name
email
avatar
}
}
`,
{ userId }
);
return (
<div>
<img src={data.user.avatar} alt={data.user.name} />
<h1>{data.user.name}</h1>
<p>{data.user.email}</p>
</div>
);
}
// 使用(Relay自带Suspense支持)
function App() {
return (
<RelayEnvironmentProvider environment={relayEnvironment}>
<Suspense fallback={<Loading />}>
<UserProfile userId="1" />
</Suspense>
</RelayEnvironmentProvider>
);
}4.4 使用Apollo Client
javascript
// Apollo Client + Suspense
import { useSuspenseQuery } from '@apollo/client';
import { gql } from '@apollo/client';
const GET_USER = gql`
query GetUser($userId: ID!) {
user(id: $userId) {
id
name
email
}
}
`;
function UserProfile({ userId }) {
const { data } = useSuspenseQuery(GET_USER, {
variables: { userId }
});
return <div>{data.user.name}</div>;
}
// 使用
function App() {
return (
<ApolloProvider client={apolloClient}>
<Suspense fallback={<Loading />}>
<UserProfile userId="1" />
</Suspense>
</ApolloProvider>
);
}注意事项
1. 避免瀑布式加载
javascript
// ❌ 瀑布式加载
function Waterfall({ userId }) {
return (
<Suspense fallback={<Loading />}>
<User userId={userId} />
</Suspense>
);
}
function User({ userId }) {
const user = use(fetchUser(userId));
// 等user加载完才开始加载posts
return (
<div>
<h1>{user.name}</h1>
<Suspense fallback={<PostsLoading />}>
<Posts userId={userId} />
</Suspense>
</div>
);
}
// ✅ 并行加载
function Parallel({ userId }) {
return (
<Suspense fallback={<Loading />}>
<UserAndPosts userId={userId} />
</Suspense>
);
}
function UserAndPosts({ userId }) {
const user = use(fetchUser(userId));
const posts = use(fetchPosts(userId)); // 同时发起
return (
<div>
<h1>{user.name}</h1>
<PostsList posts={posts} />
</div>
);
}2. 处理竞态条件
javascript
// 竞态条件问题
function SearchResults({ query }) {
const results = use(search(query));
return <Results data={results} />;
}
// 用户快速输入 "a" -> "ab" -> "abc"
// 可能 "abc" 的结果先返回,然后 "ab" 的结果覆盖它
// 解决方案:使用库(React Query、SWR)
// 它们自动处理竞态条件
// 或手动处理
function SearchResults({ query }) {
const latestQuery = useRef(query);
latestQuery.current = query;
const results = use(
search(query).then(data => {
if (latestQuery.current === query) {
return data;
}
return []; // 过期的结果
})
);
return <Results data={results} />;
}3. 缓存策略
javascript
// 考虑缓存失效
const cache = new Map();
function fetchWithCache(key, fetcher, ttl = 60000) {
const cached = cache.get(key);
if (cached && Date.now() - cached.time < ttl) {
return Promise.resolve(cached.data);
}
return fetcher().then(data => {
cache.set(key, { data, time: Date.now() });
return data;
});
}
// 主动失效缓存
function invalidateCache(key) {
cache.delete(key);
}
// 全局刷新
function refreshAll() {
cache.clear();
}常见问题
Q1: use Hook和useEffect有什么区别?
A: use在渲染时同步读取数据并可能挂起;useEffect在渲染后异步执行。
Q2: 如何避免重复请求?
A: 使用缓存或数据获取库(React Query、SWR)。
Q3: Suspense数据获取支持SSR吗?
A: React 18+支持,但需要服务器框架配合(如Next.js)。
Q4: 如何取消pending的请求?
A: 使用AbortController或数据获取库的cancel功能。
Q5: 数据更新后如何刷新?
A: 重新触发数据获取或使用库的mutation功能。
Q6: 可以在Suspense中使用多个数据源吗?
A: 可以,use Hook可以多次调用。
Q7: 如何处理认证?
A: 在fetcher中添加认证头,或使用库的配置。
Q8: 分页数据如何缓存?
A: 使用数据获取库,或手动管理Map缓存。
Q9: 如何优化慢速网络?
A: 预加载、缓存、显示骨架屏、使用Service Worker。
Q10: Suspense数据获取的最佳实践?
A: 使用成熟的库、合理缓存、避免瀑布、处理错误。
总结
核心要点
1. Suspense数据获取优势
✅ 声明式加载状态
✅ 并行请求优化
✅ 代码简洁
✅ 易于组合
2. 关键模式
✅ use Hook读取数据
✅ 并行vs嵌套加载
✅ 预加载优化
✅ 缓存管理
3. 最佳实践
✅ 使用数据获取库
✅ 避免瀑布式加载
✅ 合理的边界粒度
✅ 错误和重试处理实践建议
1. 选择合适的工具
✅ React Query: 功能全面
✅ SWR: 简单轻量
✅ Relay: GraphQL最佳
✅ Apollo: GraphQL备选
2. 性能优化
✅ 预加载关键数据
✅ 智能缓存策略
✅ 并行加载
✅ 分层Suspense
3. 用户体验
✅ 快速首屏
✅ 优雅的加载状态
✅ 错误处理
✅ 离线支持Suspense数据获取是现代React应用的核心模式,掌握它能构建更流畅的用户体验。