data-fetching-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseData Fetching Patterns
数据获取模式
Overview
概述
Data fetching is how your application retrieves data from APIs or databases. The pattern you choose affects performance, user experience, and code complexity.
数据获取是指应用从API或数据库中检索数据的方式。你选择的模式会影响性能、用户体验以及代码复杂度。
Fetching Locations
执行位置
| Where | When Executed | Use Case |
|---|---|---|
| Server (build) | Build time | Static content (SSG) |
| Server (request) | Each request | Dynamic content (SSR) |
| Client (browser) | After hydration | Interactive, real-time |
| Edge | At CDN edge | Personalization, A/B tests |
| 执行位置 | 执行时机 | 使用场景 |
|---|---|---|
| 服务端(构建时) | 构建阶段 | 静态内容(SSG) |
| 服务端(请求时) | 每次请求时 | 动态内容(SSR) |
| 客户端(浏览器) | 水合完成后 | 交互式、实时场景 |
| 边缘节点 | CDN边缘 | 个性化、A/B测试 |
Client-Side Patterns
客户端模式
1. Fetch on Render (Waterfall)
1. 渲染时获取(瀑布流问题)
Components fetch data when they mount.
jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
if (!user) return <Loading />;
return <Profile user={user} />;
}Timeline:
Component renders → useEffect runs → Fetch starts → Data arrives → Re-render
[-- Waiting --] [-- Display --]Problem: Waterfalls
jsx
function Dashboard() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(setUser); // Fetch 1
}, []);
if (!user) return <Loading />;
return (
<UserPosts userId={user.id} /> // Fetch 2 starts AFTER Fetch 1 completes
);
}
// Timeline:
// [Fetch User]───────►[Fetch Posts]───────►Display
// ↑ Can't start until user loads (waterfall)组件在挂载时获取数据。
jsx
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
if (!user) return <Loading />;
return <Profile user={user} />;
}时间线:
组件渲染 → useEffect执行 → 请求开始 → 数据到达 → 重新渲染
[-- 等待中 --] [-- 展示内容 --]问题:瀑布流
jsx
function Dashboard() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(setUser); // 请求1
}, []);
if (!user) return <Loading />;
return (
<UserPosts userId={user.id} /> // 请求2需等待请求1完成后才能开始
);
}
// 时间线:
// [获取用户数据]───────►[获取帖子数据]───────►展示内容
// ↑ 必须等用户数据加载完成才能开始(瀑布流)2. Fetch Then Render
2. 先获取再渲染
Fetch all data before rendering any component.
jsx
function Dashboard() {
const [data, setData] = useState(null);
useEffect(() => {
Promise.all([fetchUser(), fetchPosts(), fetchStats()])
.then(([user, posts, stats]) => setData({ user, posts, stats }));
}, []);
if (!data) return <Loading />;
return (
<>
<UserInfo user={data.user} />
<Posts posts={data.posts} />
<Stats stats={data.stats} />
</>
);
}Timeline:
[Fetch User ]
[Fetch Posts ]──►All Complete───►Render All
[Fetch Stats ]
↑ Parallel, but wait for slowestProblem: All-or-nothing loading. Fast data waits for slow data.
在渲染任何组件前先获取所有数据。
jsx
function Dashboard() {
const [data, setData] = useState(null);
useEffect(() => {
Promise.all([fetchUser(), fetchPosts(), fetchStats()])
.then(([user, posts, stats]) => setData({ user, posts, stats }));
}, []);
if (!data) return <Loading />;
return (
<>
<UserInfo user={data.user} />
<Posts posts={data.posts} />
<Stats stats={data.stats} />
</>
);
}时间线:
[获取用户数据 ]
[获取帖子数据 ]──►全部完成───►渲染所有组件
[获取统计数据 ]
↑ 并行执行,但需等待最慢的请求问题:全有或全无的加载方式。快速返回的数据需等待慢请求完成。
3. Render as You Fetch (Concurrent)
3. 边渲染边获取(并发模式)
Start fetching immediately, render components as data arrives.
jsx
// Start fetches immediately (not in useEffect)
const userPromise = fetchUser();
const postsPromise = fetchPosts();
function Dashboard() {
return (
<Suspense fallback={<UserSkeleton />}>
<UserInfo userPromise={userPromise} />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<Posts postsPromise={postsPromise} />
</Suspense>
);
}
// Each component renders when its data is ready
function UserInfo({ userPromise }) {
const user = use(userPromise); // React 19's use() hook
return <div>{user.name}</div>;
}Timeline:
[Fetch User ]───►Render User
[Fetch Posts ]─────────►Render Posts
↑ Independent, progressive rendering立即开始请求,数据到达后就渲染对应组件。
jsx
// 立即开始请求(不在useEffect中)
const userPromise = fetchUser();
const postsPromise = fetchPosts();
function Dashboard() {
return (
<Suspense fallback={<UserSkeleton />}>
<UserInfo userPromise={userPromise} />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<Posts postsPromise={postsPromise} />
</Suspense>
);
}
// 每个组件在数据就绪后渲染
function UserInfo({ userPromise }) {
const user = use(userPromise); // React 19的use()钩子
return <div>{user.name}</div>;
}时间线:
[获取用户数据 ]───►渲染用户组件
[获取帖子数据 ]─────────►渲染帖子组件
↑ 独立执行,渐进式渲染Server-Side Patterns
服务端模式
Server Data Fetching (SSR)
服务端数据获取(SSR)
Data fetched on server before sending HTML:
javascript
// Next.js App Router
async function ProductPage({ params }) {
const product = await db.products.findById(params.id); // Runs on server
return <ProductDetails product={product} />;
}Benefits:
- No loading state (data already in HTML)
- SEO-friendly (content in initial HTML)
- Direct database/API access
在服务端获取数据后再发送HTML:
javascript
// Next.js App Router
async function ProductPage({ params }) {
const product = await db.products.findById(params.id); // 在服务端执行
return <ProductDetails product={product} />;
}优势:
- 无加载状态(数据已包含在HTML中)
- 对SEO友好(初始HTML中包含内容)
- 可直接访问数据库/API
Parallel Data Fetching
并行数据获取
Fetch multiple resources simultaneously:
javascript
// Bad: Sequential (waterfall)
const user = await getUser();
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
// Good: Parallel where possible
const [user, stats] = await Promise.all([
getUser(),
getStats(),
]);
// Then sequential for dependent data
const posts = await getPosts(user.id);同时获取多个资源:
javascript
// 错误示例:串行执行(瀑布流)
const user = await getUser();
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
// 正确示例:尽可能并行执行
const [user, stats] = await Promise.all([
getUser(),
getStats(),
]);
// 然后串行执行依赖数据的请求
const posts = await getPosts(user.id);Streaming and Suspense
流式传输与Suspense
Stream HTML to the browser as soon as each part of your UI is ready—even if it's out of order—letting users see content without having to wait for everything.
- Progressive & Out-of-Order Streaming: With React's server components and Suspense, the server can send parts of the page (like headers or footers) immediately, even if other parts (like a slow data section) are still loading. This means sections of your UI don't have to stream in their coded order—each streams independently as soon as its data is ready.
- Suspense Boundaries & Placeholders: The boundary tells React where some data may take longer. While waiting, a fallback component (such as
<Suspense>) is sent as a placeholder in the HTML.<Loading /> - Client-Side JS Swapping: Along with the initial HTML and placeholders, React also sends a small client-side JavaScript "runtime" that listens for streamed content. When the slow component's data arrives from the server, this client JS automatically swaps out the placeholder fallback for the real content—right in place, without a full page reload.
jsx
// Example: Immediate and slow components streamed out of order
async function Page() {
return (
<div>
<Header />
{/* <Header> streams to client immediately */}
<Suspense fallback={<Loading />}>
<SlowComponent />
{/* Placeholder <Loading /> is shown, and client JS
will replace it with <SlowComponent> content
as soon as the server streams it */}
</Suspense>
<Footer />
{/* <Footer> can stream immediately,
before or after <SlowComponent>, depending on what finishes first */}
</div>
);
}React's streaming SSR means the server sends "islands" of ready UI as soon as they're done, wherever they are in the code. The browser shows the page piecemeal, filling in slow spots later. The React client runtime handles swapping placeholders for finished components when each chunk of streamed content completes, providing a smooth and efficient user experience.
undefined一旦UI的各个部分准备就绪,就将HTML流式传输到浏览器——即使顺序打乱也没关系——让用户无需等待所有内容加载完成就能看到部分内容。
- 渐进式与乱序流式传输: 通过React服务端组件和Suspense,服务端可以立即发送页面的部分内容(如页眉或页脚),即使其他部分(如加载缓慢的数据区块)仍在处理中。这意味着UI的各个部分无需按照代码顺序流式传输——每个部分在数据就绪后就独立流式传输。
- Suspense边界与占位符: 边界告知React哪些数据可能需要较长时间加载。在等待期间,会在HTML中发送一个回退组件(如
<Suspense>)作为占位符。<Loading /> - 客户端JS替换: 除了初始HTML和占位符,React还会发送一个小型客户端JavaScript“运行时”,用于监听流式传输的内容。当慢组件的数据从服务端到达时,这个客户端JS会自动将占位符替换为真实内容——无需整页刷新。
jsx
// 示例:立即就绪和加载缓慢的组件乱序流式传输
async function Page() {
return (
<div>
<Header />
{/* <Header>立即流式传输到客户端 */}
<Suspense fallback={<Loading />}>
<SlowComponent />
{/* 显示占位符<Loading />,当服务端流式传输完成后,客户端JS
会将其替换为<SlowComponent>的内容 */}
</Suspense>
<Footer />
{/* <Footer>可立即流式传输,
可能在<SlowComponent>之前或之后,取决于哪个先完成 */}
</div>
);
}React的流式SSR意味着服务端在UI的各个部分准备好后就发送“独立区块”,无论它们在代码中的顺序如何。浏览器逐步显示页面内容,稍后再填充加载缓慢的区域。React客户端运行时负责在每个流式传输区块完成时将占位符替换为已完成的组件,提供流畅高效的用户体验。
undefinedCaching Patterns
缓存模式
Request Deduplication
请求去重
Multiple components requesting same data should share request:
javascript
// Without deduplication
// Component A: fetch('/api/user')
// Component B: fetch('/api/user')
// = 2 requests
// With deduplication (React cache / TanStack Query)
// Component A: useQuery(['user'], fetchUser)
// Component B: useQuery(['user'], fetchUser)
// = 1 request, shared result多个组件请求相同数据时应共享请求:
javascript
// 无请求去重
// 组件A: fetch('/api/user')
// 组件B: fetch('/api/user')
// = 2次请求
// 有请求去重(React cache / TanStack Query)
// 组件A: useQuery(['user'], fetchUser)
// 组件B: useQuery(['user'], fetchUser)
// = 1次请求,共享结果Stale-While-Revalidate
过期时重新验证(Stale-While-Revalidate)
Show cached data immediately, update in background:
javascript
const { data } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
staleTime: 60000, // Fresh for 60s
refetchOnMount: true, // Background refetch
});
// Timeline:
// 1. Show cached data immediately
// 2. Check if stale
// 3. If stale, refetch in background
// 4. Update UI when fresh data arrives立即显示缓存数据,在后台更新:
javascript
const { data } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
staleTime: 60000, // 60秒内视为新鲜数据
refetchOnMount: true, // 后台重新获取
});
// 时间线:
// 1. 立即显示缓存数据
// 2. 检查数据是否过期
// 3. 如果过期,在后台重新获取
// 4. 新鲜数据到达后更新UICache Invalidation
缓存失效
Clear or update cache after mutations:
javascript
const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries(['posts']);
// Or update cache directly
queryClient.setQueryData(['posts'], (old) => [...old, newPost]);
},
});在数据变更后清除或更新缓存:
javascript
const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
// 失效并重新获取
queryClient.invalidateQueries(['posts']);
// 或直接更新缓存
queryClient.setQueryData(['posts'], (old) => [...old, newPost]);
},
});Loading State Patterns
加载状态模式
Skeleton Screens
骨架屏
Show content structure while loading:
jsx
function ProductCard({ product }) {
if (!product) {
return (
<div className="card">
<div className="skeleton-image" />
<div className="skeleton-text" />
<div className="skeleton-text short" />
</div>
);
}
return (
<div className="card">
<img src={product.image} />
<h3>{product.name}</h3>
<p>{product.price}</p>
</div>
);
}加载时显示内容结构:
jsx
function ProductCard({ product }) {
if (!product) {
return (
<div className="card">
<div className="skeleton-image" />
<div className="skeleton-text" />
<div className="skeleton-text short" />
</div>
);
}
return (
<div className="card">
<img src={product.image} />
<h3>{product.name}</h3>
<p>{product.price}</p>
</div>
);
}Optimistic Updates
乐观更新
Update UI immediately, rollback on error:
javascript
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries(['todos']);
// Snapshot previous value
const previous = queryClient.getQueryData(['todos']);
// Optimistically update
queryClient.setQueryData(['todos'], (old) =>
old.map(t => t.id === newTodo.id ? newTodo : t)
);
return { previous };
},
onError: (err, newTodo, context) => {
// Rollback on error
queryClient.setQueryData(['todos'], context.previous);
},
});立即更新UI,出错时回滚:
javascript
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// 取消正在进行的重新获取
await queryClient.cancelQueries(['todos']);
// 快照之前的值
const previous = queryClient.getQueryData(['todos']);
// 乐观更新
queryClient.setQueryData(['todos'], (old) =>
old.map(t => t.id === newTodo.id ? newTodo : t)
);
return { previous };
},
onError: (err, newTodo, context) => {
// 出错时回滚
queryClient.setQueryData(['todos'], context.previous);
},
});Placeholder Data
占位数据
Show estimated/placeholder data while fetching:
javascript
const { data } = useQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id),
placeholderData: {
name: 'Loading...',
price: '--',
image: '/placeholder.jpg',
},
});获取数据时显示预估/占位数据:
javascript
const { data } = useQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id),
placeholderData: {
name: '加载中...',
price: '--',
image: '/placeholder.jpg',
},
});Error Handling
错误处理
Error Boundaries
错误边界
Catch and display errors gracefully:
jsx
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</ErrorBoundary>优雅捕获并显示错误:
jsx
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</ErrorBoundary>Retry Strategies
重试策略
javascript
const { data } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
retry: 3, // Retry 3 times
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000), // Exponential backoff
});javascript
const { data } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
retry: 3, // 重试3次
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000), // 指数退避
});Pattern Comparison
模式对比
| Pattern | First Content | Data Freshness | Complexity |
|---|---|---|---|
| Client fetch (useEffect) | Slow | Real-time capable | Low |
| SSR | Fast | Request-time | Medium |
| SSG | Fastest | Build-time | Low |
| ISR | Fastest | Periodic | Medium |
| Streaming | Progressive | Request-time | Medium |
| 模式 | 首次内容展示速度 | 数据新鲜度 | 复杂度 |
|---|---|---|---|
| 客户端获取(useEffect) | 慢 | 支持实时更新 | 低 |
| SSR | 快 | 请求时新鲜 | 中 |
| SSG | 最快 | 构建时新鲜 | 低 |
| ISR | 最快 | 定期更新 | 中 |
| 流式传输 | 渐进式 | 请求时新鲜 | 中 |
Decision Framework
决策框架
Is the data static or rarely changes?
├── Yes → SSG or ISR
│
└── No → Does the page need SEO?
├── Yes → SSR or Streaming
│
└── No → Does it need real-time updates?
├── Yes → Client-side + WebSocket/polling
│
└── No → Is initial load critical?
├── Yes → SSR + Client hydration
└── No → Client-side fetching数据是否为静态或极少变更?
├── 是 → SSG或ISR
│
└── 否 → 页面是否需要SEO?
├── 是 → SSR或流式传输
│
└── 否 → 是否需要实时更新?
├── 是 → 客户端获取 + WebSocket/轮询
│
└── 否 → 初始加载速度是否关键?
├── 是 → SSR + 客户端水合
└── 否 → 客户端获取Anti-Patterns
反模式
1. Fetching in useEffect Without Cleanup
1. useEffect中获取数据但不清理
javascript
// Bad: Race condition possible
useEffect(() => {
fetchData().then(setData);
}, [id]);
// Good: Cleanup or use library
useEffect(() => {
let cancelled = false;
fetchData().then(data => {
if (!cancelled) setData(data);
});
return () => { cancelled = true; };
}, [id]);javascript
// 错误示例:可能存在竞态条件
useEffect(() => {
fetchData().then(setData);
}, [id]);
// 正确示例:清理或使用库
useEffect(() => {
let cancelled = false;
fetchData().then(data => {
if (!cancelled) setData(data);
});
return () => { cancelled = true; };
}, [id]);2. Not Handling Loading/Error States
2. 不处理加载/错误状态
javascript
// Bad: No loading or error handling
const { data } = useQuery(['products'], fetchProducts);
return <ProductList products={data} />; // data might be undefined
// Good: Handle all states
if (isLoading) return <Loading />;
if (error) return <Error message={error.message} />;
return <ProductList products={data} />;javascript
// 错误示例:未处理加载或错误状态
const { data } = useQuery(['products'], fetchProducts);
return <ProductList products={data} />; // data可能为undefined
// 正确示例:处理所有状态
if (isLoading) return <Loading />;
if (error) return <Error message={error.message} />;
return <ProductList products={data} />;3. Over-fetching
3. 过度获取
javascript
// Bad: Fetching more than needed
const { data: user } = useQuery(['user'], () =>
fetch('/api/users/full-profile') // 50 fields
);
// Only using: user.name, user.avatar
// Good: Fetch only what's needed (or use GraphQL)
const { data: user } = useQuery(['user-summary'], () =>
fetch('/api/users/summary') // 3 fields
);javascript
// 错误示例:获取超出需求的数据
const { data: user } = useQuery(['user'], () =>
fetch('/api/users/full-profile') // 50个字段
);
// 仅使用:user.name, user.avatar
// 正确示例:仅获取所需数据(或使用GraphQL)
const { data: user } = useQuery(['user-summary'], () =>
fetch('/api/users/summary') // 3个字段
);Deep Dive: Understanding Data Fetching From First Principles
深度解析:从底层原理理解数据获取
The Network Request Lifecycle
网络请求生命周期
Every data fetch involves multiple steps:
CLIENT NETWORK SERVER
1. fetch() called
│
2. DNS Lookup ─────────────────► DNS Server
│ │
│◄──────────────────── IP: 93.184.216.34
│
3. TCP Handshake ─────────────────────────────────────────────► SYN
│ │
│◄─────────────────────────────────────────────────── SYN-ACK
│
│───────────────────────────────────────────────────► ACK
│
4. TLS Handshake (HTTPS) ─────────────────────────────────────►
│◄───────────────────────────────────────────────────
│
5. HTTP Request ──────────────────────────────────────────────►
│ GET /api/data HTTP/1.1
│ Host: example.com │
│ ▼
│ 6. Server processes
│ │
│ 7. Query database
│ │
│ 8. Build response
│ │
│◄────────────────────────────────────────────────────
│ HTTP/1.1 200 OK
│ {"data": [...]}
│
9. Parse response
│
10. Update state
│
11. Re-render Time breakdown (typical):
DNS Lookup: 10-100ms (cached: 0ms)
TCP Handshake: 50-100ms (1 round trip)
TLS Handshake: 50-150ms (2 round trips)
Request/Response: 50-500ms (depends on server + data size)
Parsing: 1-10ms
Rendering: 5-50ms
────────────────────────────────
TOTAL: 166-910ms每个数据获取都包含多个步骤:
客户端 网络 服务端
1. 调用fetch()
│
2. DNS查询 ─────────────────► DNS服务器
│ │
│◄──────────────────── IP: 93.184.216.34
│
3. TCP握手 ─────────────────────────────────────────────► SYN
│ │
│◄─────────────────────────────────────────────────── SYN-ACK
│
│───────────────────────────────────────────────────► ACK
│
4. TLS握手(HTTPS) ─────────────────────────────────────►
│◄───────────────────────────────────────────────────
│
5. HTTP请求 ──────────────────────────────────────────────►
│ GET /api/data HTTP/1.1
│ Host: example.com │
│ ▼
│ 6. 服务端处理
│ │
│ 7. 查询数据库
│ │
│ 8. 构建响应
│ │
│◄────────────────────────────────────────────────────
│ HTTP/1.1 200 OK
│ {"data": [...]}
│
9. 解析响应
│
10. 更新状态
│
11. 重新渲染 时间占比(典型情况):
DNS查询: 10-100ms(缓存时:0ms)
TCP握手: 50-100ms(1次往返)
TLS握手: 50-150ms(2次往返)
请求/响应: 50-500ms(取决于服务端+数据大小)
解析: 1-10ms
渲染: 5-50ms
────────────────────────────────
总计: 166-910msThe Waterfall Problem Explained
瀑布流问题解析
Waterfalls occur when requests depend on each other:
javascript
// WATERFALL CODE:
async function loadDashboard() {
const user = await fetchUser(); // 200ms ─────┐
const posts = await fetchPosts(user.id); // 300ms ─────┼─► WAIT
const comments = await fetchComments(posts[0].id); // 250ms ─┘
}
// TIMELINE (serial):
// |── fetchUser (200ms) ──|── fetchPosts (300ms) ──|── fetchComments (250ms) ──|
// Total: 750ms
// PARALLEL CODE:
async function loadDashboard() {
// Start all independent fetches simultaneously
const [user, globalPosts] = await Promise.all([
fetchUser(), // 200ms ──┐
fetchGlobalPosts(), // 300ms ──┤► PARALLEL
]); // └► Wait for slowest: 300ms
// Dependent fetch still sequential
const userPosts = await fetchUserPosts(user.id); // 250ms
}
// TIMELINE (parallel where possible):
// |── fetchUser (200ms) ───|
// |── fetchGlobalPosts (300ms) ──|── fetchUserPosts (250ms) ──|
// Total: 550ms (vs 750ms waterfall)当请求相互依赖时会出现瀑布流:
javascript
// 瀑布流代码:
async function loadDashboard() {
const user = await fetchUser(); // 200ms ─────┐
const posts = await fetchPosts(user.id); // 300ms ─────┼─► 等待
const comments = await fetchComments(posts[0].id); // 250ms ─┘
}
// 时间线(串行):
// |── 获取用户数据(200ms) ──|── 获取帖子数据(300ms) ──|── 获取评论数据(250ms) ──|
// 总计: 750ms
// 并行代码:
async function loadDashboard() {
// 同时启动所有独立请求
const [user, globalPosts] = await Promise.all([
fetchUser(), // 200ms ──┐
fetchGlobalPosts(), // 300ms ──┤► 并行
]); // └► 等待最慢的请求: 300ms
// 依赖请求仍需串行执行
const userPosts = await fetchUserPosts(user.id); // 250ms
}
// 时间线(尽可能并行):
// |── 获取用户数据(200ms) ───|
// |── 获取全局帖子数据(300ms) ──|── 获取用户帖子数据(250ms) ──|
// 总计: 550ms(对比瀑布流的750ms)Why Caching is Essential
缓存的重要性
Without caching, you make redundant requests:
javascript
// SCENARIO: Multiple components need user data
function Header() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user').then(r => r.json()).then(setUser);
}, []);
return <span>{user?.name}</span>;
}
function Sidebar() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user').then(r => r.json()).then(setUser); // SAME REQUEST!
}, []);
return <img src={user?.avatar} />;
}
function Dashboard() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user').then(r => r.json()).then(setUser); // SAME REQUEST!
}, []);
return <span>Welcome, {user?.name}</span>;
}
// RESULT: 3 identical network requests!
// Each takes 200ms, wasting bandwidth and timeRequest deduplication:
javascript
// WITH TANSTACK QUERY:
function Header() {
const { data: user } = useQuery(['user'], fetchUser);
// First component to request starts the fetch
}
function Sidebar() {
const { data: user } = useQuery(['user'], fetchUser);
// Same query key = shares the same request/cache
}
function Dashboard() {
const { data: user } = useQuery(['user'], fetchUser);
// All three components share ONE network request
}
// RESULT: 1 network request, shared across components没有缓存的话,会产生冗余请求:
javascript
// 场景:多个组件需要用户数据
function Header() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user').then(r => r.json()).then(setUser);
}, []);
return <span>{user?.name}</span>;
}
function Sidebar() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user').then(r => r.json()).then(setUser); // 相同请求!
}, []);
return <img src={user?.avatar} />;
}
function Dashboard() {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user').then(r => r.json()).then(setUser); // 相同请求!
}, []);
return <span>欢迎, {user?.name}</span>;
}
// 结果: 3次完全相同的网络请求!
// 每次耗时200ms,浪费带宽和时间请求去重:
javascript
// 使用TanStack Query:
function Header() {
const { data: user } = useQuery(['user'], fetchUser);
// 第一个发起请求的组件启动请求
}
function Sidebar() {
const { data: user } = useQuery(['user'], fetchUser);
// 相同查询键 = 共享同一请求/缓存
}
function Dashboard() {
const { data: user } = useQuery(['user'], fetchUser);
// 三个组件共享一次网络请求
}
// 结果: 1次网络请求,在组件间共享How Cache Invalidation Works
缓存失效的工作原理
Caches must be invalidated when data changes:
javascript
// PROBLEM: Stale data after mutation
// User edits their profile
await updateProfile({ name: 'New Name' });
// But cached user data still shows old name!
// Other components display stale data
// SOLUTION 1: Invalidate and refetch
const queryClient = useQueryClient();
async function handleUpdate(newData) {
await updateProfile(newData);
// Invalidate the cache - next access will refetch
queryClient.invalidateQueries(['user']);
}
// SOLUTION 2: Optimistic update + invalidate
async function handleUpdate(newData) {
// Immediately update cache (optimistic)
queryClient.setQueryData(['user'], (old) => ({
...old,
...newData,
}));
// Then persist to server
try {
await updateProfile(newData);
// Invalidate to get any server-side changes
queryClient.invalidateQueries(['user']);
} catch (error) {
// Rollback on failure
queryClient.setQueryData(['user'], originalData);
}
}
// SOLUTION 3: Server returns updated data
async function handleUpdate(newData) {
const updatedUser = await updateProfile(newData);
// Server returns the new state - update cache directly
queryClient.setQueryData(['user'], updatedUser);
}当数据变更时必须失效缓存:
javascript
// 问题:数据变更后缓存过期
// 用户编辑个人资料
await updateProfile({ name: '新名称' });
// 但缓存的用户数据仍显示旧名称!
// 其他组件显示过期数据
// 解决方案1:失效并重新获取
const queryClient = useQueryClient();
async function handleUpdate(newData) {
await updateProfile(newData);
// 失效缓存 - 下次访问时重新获取
queryClient.invalidateQueries(['user']);
}
// 解决方案2:乐观更新 + 失效
async function handleUpdate(newData) {
// 立即更新缓存(乐观更新)
queryClient.setQueryData(['user'], (old) => ({
...old,
...newData,
}));
// 然后持久化到服务端
try {
await updateProfile(newData);
// 成功! 状态已正确
queryClient.invalidateQueries(['user']);
} catch (error) {
// 失败! 回滚
queryClient.setQueryData(['user'], originalData);
}
}
// 解决方案3:服务端返回更新后的数据
async function handleUpdate(newData) {
const updatedUser = await updateProfile(newData);
// 服务端返回新状态 - 直接更新缓存
queryClient.setQueryData(['user'], updatedUser);
}Race Conditions in Data Fetching
数据获取中的竞态条件
When fetches can overlap, you get race conditions:
javascript
// THE BUG:
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(setResults);
}, [query]);
return <ResultList items={results} />;
}
// SCENARIO:
// User types "re" - starts fetch A (takes 500ms)
// User types "react" - starts fetch B (takes 200ms)
// Fetch B completes first - shows "react" results
// Fetch A completes second - OVERWRITES with "re" results!
// User sees wrong results!
// THE FIX: Abort previous request
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(r => r.json())
.then(setResults)
.catch(e => {
if (e.name === 'AbortError') return; // Expected
throw e;
});
// Cleanup: abort if query changes or component unmounts
return () => controller.abort();
}, [query]);
return <ResultList items={results} />;
}
// OR use a library that handles this:
function SearchResults({ query }) {
const { data: results } = useQuery({
queryKey: ['search', query],
queryFn: () => fetch(`/api/search?q=${query}`).then(r => r.json()),
// TanStack Query automatically cancels outdated requests
});
}当请求重叠时会出现竞态条件:
javascript
// 错误示例:
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(setResults);
}, [query]);
return <ResultList items={results} />;
}
// 场景:
// 用户输入"re" - 启动请求A(耗时500ms)
// 用户输入"react" - 启动请求B(耗时200ms)
// 请求B先完成 - 显示"react"结果
// 请求A后完成 - 覆盖为"re"的结果!
// 用户看到错误结果!
// 修复方案:中止之前的请求
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(r => r.json())
.then(setResults)
.catch(e => {
if (e.name === 'AbortError') return; // 预期情况
throw e;
});
// 清理:当查询变更或组件卸载时中止
return () => controller.abort();
}, [query]);
return <ResultList items={results} />;
}
// 或使用处理该问题的库:
function SearchResults({ query }) {
const { data: results } = useQuery({
queryKey: ['search', query],
queryFn: () => fetch(`/api/search?q=${query}`).then(r => r.json()),
// TanStack Query自动取消过时请求
});
}Suspense: A New Data Fetching Paradigm
Suspense:新的数据获取范式
Traditional approach: component manages its loading state
Suspense approach: component delegates loading to boundary
javascript
// TRADITIONAL: Each component handles loading
function ProductPage() {
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchProduct()
.then(setProduct)
.finally(() => setLoading(false));
}, []);
if (loading) return <Spinner />; // Loading logic in component
return <Product data={product} />;
}
// SUSPENSE: Loading delegated to boundary
function ProductPage() {
const product = use(fetchProduct()); // Suspends if not ready
return <Product data={product} />; // Only renders when ready
}
// Boundary handles loading
function App() {
return (
<Suspense fallback={<Spinner />}> {/* Loading UI here */}
<ProductPage />
</Suspense>
);
}How Suspense works internally:
javascript
// CONCEPTUAL MECHANISM:
function use(promise) {
// Check if promise is already resolved
if (promise.status === 'fulfilled') {
return promise.value;
}
if (promise.status === 'rejected') {
throw promise.reason;
}
// Not yet resolved - THROW the promise!
throw promise;
}
// React catches the thrown promise
// Shows nearest Suspense fallback
// When promise resolves, re-renders the component
// This is why Suspense only works with:
// - React's use() hook
// - Libraries that support Suspense (like TanStack Query)
// - Server Components传统方式:组件管理自身加载状态
Suspense方式:组件将加载逻辑委托给边界
javascript
// 传统方式:每个组件处理加载逻辑
function ProductPage() {
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchProduct()
.then(setProduct)
.finally(() => setLoading(false));
}, []);
if (loading) return <Spinner />; // 组件内的加载逻辑
return <Product data={product} />;
}
// Suspense方式:加载逻辑委托给边界
function ProductPage() {
const product = use(fetchProduct()); // 未就绪时挂起
return <Product data={product} />; // 仅在就绪时渲染
}
// 边界处理加载逻辑
function App() {
return (
<Suspense fallback={<Spinner />}> {/* 加载UI在此处 */}
<ProductPage />
</Suspense>
);
}Suspense内部工作原理:
javascript
// 概念性机制:
function use(promise) {
// 检查Promise是否已解析
if (promise.status === 'fulfilled') {
return promise.value;
}
if (promise.status === 'rejected') {
throw promise.reason;
}
// 未解析 - 抛出Promise!
throw promise;
}
// React捕获抛出的Promise
// 显示最近的Suspense回退组件
// 当Promise解析后,重新渲染组件
// 这就是为什么Suspense仅适用于:
// - React的use()钩子
// - 支持Suspense的库(如TanStack Query)
// - 服务端组件Streaming: Progressive Data Loading
流式传输:渐进式数据加载
Streaming sends data as it becomes available:
javascript
// TRADITIONAL SSR:
// Server must fetch ALL data before sending ANY HTML
async function ProductPage() {
// These all must complete before response starts
const product = await getProduct(); // 100ms
const reviews = await getReviews(); // 500ms ← SLOW!
const related = await getRelatedProducts(); // 200ms
// Total wait: 800ms before ANY HTML sent
return (
<div>
<Product data={product} />
<Reviews data={reviews} />
<Related data={related} />
</div>
);
}
// STREAMING SSR:
// Send what's ready, stream the rest
async function ProductPage() {
const product = await getProduct(); // 100ms - quick!
return (
<div>
<Product data={product} /> {/* Sent immediately */}
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews /> {/* Streams when ready */}
</Suspense>
<Suspense fallback={<RelatedSkeleton />}>
<Related /> {/* Streams when ready */}
</Suspense>
</div>
);
}
// Client receives:
// t=100ms: Product HTML + skeleton placeholders
// t=300ms: Related products stream in
// t=600ms: Reviews stream in
// User sees content progressively!流式传输在数据就绪时就发送:
javascript
// 传统SSR:
// 服务端必须获取所有数据后才能发送任何HTML
async function ProductPage() {
// 这些都必须完成后才能开始响应
const product = await getProduct(); // 100ms
const reviews = await getReviews(); // 500ms ← 慢!
const related = await getRelatedProducts(); // 200ms
// 总等待时间: 800ms后才能发送任何HTML
return (
<div>
<Product data={product} />
<Reviews data={reviews} />
<Related data={related} />
</div>
);
}
// 流式SSR:
// 发送已就绪的内容,其余内容流式传输
async function ProductPage() {
const product = await getProduct(); // 100ms - 快速!
return (
<div>
<Product data={product} /> {/* 立即发送 */}
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews /> {/* 就绪后流式传输 */}
</Suspense>
<Suspense fallback={<RelatedSkeleton />}>
<Related /> {/* 就绪后流式传输 */}
</Suspense>
</div>
);
}
// 客户端接收:
// t=100ms: 产品HTML + 骨架占位符
// t=300ms: 相关产品流式传输完成
// t=600ms: 评论流式传输完成
// 用户渐进式看到内容!Optimistic Updates: The UX Advantage
乐观更新:UX优势
Optimistic updates show expected result before server confirms:
javascript
// WITHOUT OPTIMISTIC UPDATE:
// User clicks "Like"
// Spinner shows for 200ms
// Heart fills in
// Feels laggy
async function handleLike() {
setIsLoading(true);
await api.likePost(postId); // 200ms
await refetchPost(); // Another 200ms
setIsLoading(false);
// 400ms of waiting!
}
// WITH OPTIMISTIC UPDATE:
// User clicks "Like"
// Heart fills in IMMEDIATELY
// Server confirms in background
async function handleLike() {
// Immediately update local state
setIsLiked(true);
setLikeCount(c => c + 1);
try {
await api.likePost(postId);
// Success! State is already correct
} catch (error) {
// Failure! Revert the optimistic update
setIsLiked(false);
setLikeCount(c => c - 1);
showError('Failed to like post');
}
}
// User perceives INSTANT response
// Only 1 in 100 interactions might need rollback乐观更新在服务端确认前显示预期结果:
javascript
// 无乐观更新:
// 用户点击"点赞"
// 加载图标显示200ms
// 心形图标填充
// 感觉卡顿
async function handleLike() {
setIsLoading(true);
await api.likePost(postId); // 200ms
await refetchPost(); // 又200ms
setIsLoading(false);
// 等待400ms!
}
// 有乐观更新:
// 用户点击"点赞"
// 心形图标立即填充
// 服务端在后台确认
async function handleLike() {
// 立即更新本地状态
setIsLiked(true);
setLikeCount(c => c + 1);
try {
await api.likePost(postId);
// 成功! 状态已正确
} catch (error) {
// 失败! 回滚乐观更新
setIsLiked(false);
setLikeCount(c => c - 1);
showError('点赞失败');
}
}
// 用户感知到即时响应
// 仅1%的交互可能需要回滚GraphQL vs REST: Data Fetching Implications
GraphQL vs REST: 数据获取影响
Different protocols have different fetching patterns:
javascript
// REST: Multiple endpoints, potential over/under-fetching
// Need user profile + their posts + comment counts
// Requires 3 requests:
const user = await fetch('/api/users/123');
const posts = await fetch('/api/users/123/posts');
const comments = await fetch('/api/users/123/posts/comments/count');
// Over-fetching: Each endpoint returns all fields
// User endpoint returns 50 fields, you need 3
// GRAPHQL: Single endpoint, precise data
const query = `
query UserProfile($id: ID!) {
user(id: $id) {
name # Only fields you need
avatar
posts {
title
commentCount
}
}
}
`;
const { user } = await graphqlFetch(query, { id: '123' });
// One request, exact data shape you need
// TRADEOFFS:
// REST:
// + Simple, well-understood
// + HTTP caching works naturally
// + Each endpoint cacheable independently
// - Over/under-fetching
// - Multiple round trips
// GRAPHQL:
// + Fetch exactly what you need
// + Single request
// + Strongly typed
// - More complex server setup
// - Caching more complex
// - Potential for expensive queries不同协议有不同的获取模式:
javascript
// REST: 多个端点,可能过度/不足获取
// 需要用户资料 + 他们的帖子 + 评论数
// 需要3次请求:
const user = await fetch('/api/users/123');
const posts = await fetch('/api/users/123/posts');
const comments = await fetch('/api/users/123/posts/comments/count');
// 过度获取: 每个端点返回所有字段
// 用户端点返回50个字段,你只需要3个
// GRAPHQL: 单个端点,精确数据
const query = `
query UserProfile($id: ID!) {
user(id: $id) {
name # 仅获取需要的字段
avatar
posts {
title
commentCount
}
}
}
`;
const { user } = await graphqlFetch(query, { id: '123' });
// 一次请求,获取精确的数据结构
// 权衡:
// REST:
// + 简单,易于理解
// + HTTP缓存自然生效
// + 每个端点可独立缓存
// - 过度/不足获取
// - 多次往返
// GRAPHQL:
// + 精确获取所需数据
// + 单次请求
// + 强类型
// - 服务端设置更复杂
// - 缓存更复杂
// - 可能存在昂贵查询Error Handling Strategies
错误处理策略
Different errors need different handling:
javascript
// ERROR TYPES:
// 1. Network errors (offline, timeout)
try {
await fetch('/api/data');
} catch (e) {
if (!navigator.onLine) {
showToast('You appear to be offline');
// Maybe return cached data
return cache.get('data');
}
if (e.name === 'AbortError') {
// Request was cancelled, ignore
return;
}
throw e;
}
// 2. HTTP errors (4xx, 5xx)
const response = await fetch('/api/data');
if (!response.ok) {
if (response.status === 401) {
// Unauthorized - redirect to login
router.push('/login');
return;
}
if (response.status === 404) {
// Not found - show not found UI
setNotFound(true);
return;
}
if (response.status >= 500) {
// Server error - retry later
throw new Error('Server error, please try again');
}
}
// 3. Application errors (in response body)
const data = await response.json();
if (data.error) {
// Business logic error
showError(data.error.message);
return;
}
// ERROR BOUNDARY PATTERN:
<ErrorBoundary
fallback={<ErrorPage />}
onError={(error) => logToSentry(error)}
>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</ErrorBoundary>不同错误需要不同处理方式:
javascript
// 错误类型:
// 1. 网络错误(离线、超时)
try {
await fetch('/api/data');
} catch (e) {
if (!navigator.onLine) {
showToast('你似乎处于离线状态');
// 可能返回缓存数据
return cache.get('data');
}
if (e.name === 'AbortError') {
// 请求已取消,忽略
return;
}
throw e;
}
// 2. HTTP错误(4xx, 5xx)
const response = await fetch('/api/data');
if (!response.ok) {
if (response.status === 401) {
// 未授权 - 重定向到登录页
router.push('/login');
return;
}
if (response.status === 404) {
// 未找到 - 显示未找到UI
setNotFound(true);
return;
}
if (response.status >= 500) {
// 服务端错误 - 稍后重试
throw new Error('服务端错误,请稍后重试');
}
}
// 3. 应用错误(响应体中)
const data = await response.json();
if (data.error) {
// 业务逻辑错误
showError(data.error.message);
return;
}
// 错误边界模式:
<ErrorBoundary
fallback={<ErrorPage />}
onError={(error) => logToSentry(error)}
>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</ErrorBoundary>The Request Deduplication Deep Dive
请求去重深度解析
How libraries prevent duplicate requests:
javascript
// NAIVE IMPLEMENTATION:
const cache = new Map();
const inFlight = new Map();
async function dedupedFetch(key, fetchFn) {
// Return cached data if fresh
if (cache.has(key)) {
const { data, timestamp } = cache.get(key);
if (Date.now() - timestamp < STALE_TIME) {
return data;
}
}
// If request already in flight, wait for it
if (inFlight.has(key)) {
return inFlight.get(key);
}
// Start new request
const promise = fetchFn().then(data => {
cache.set(key, { data, timestamp: Date.now() });
inFlight.delete(key);
return data;
});
inFlight.set(key, promise);
return promise;
}
// USAGE:
// Both calls share the SAME network request
const data1 = await dedupedFetch('user', () => fetch('/api/user'));
const data2 = await dedupedFetch('user', () => fetch('/api/user'));库如何防止重复请求:
javascript
// 简单实现:
const cache = new Map();
const inFlight = new Map();
async function dedupedFetch(key, fetchFn) {
// 如果数据新鲜,返回缓存数据
if (cache.has(key)) {
const { data, timestamp } = cache.get(key);
if (Date.now() - timestamp < STALE_TIME) {
return data;
}
}
// 如果请求正在进行中,等待它完成
if (inFlight.has(key)) {
return inFlight.get(key);
}
// 发起新请求
const promise = fetchFn().then(data => {
cache.set(key, { data, timestamp: Date.now() });
inFlight.delete(key);
return data;
});
inFlight.set(key, promise);
return promise;
}
// 使用:
// 两次调用共享同一网络请求
const data1 = await dedupedFetch('user', () => fetch('/api/user'));
const data2 = await dedupedFetch('user', () => fetch('/api/user'));For Framework Authors: Building Data Fetching Systems
框架开发者指南:构建数据获取系统
Implementation Note: The patterns and code examples below represent one proven approach to building data fetching systems. Different frameworks take different approaches—Remix uses loaders, React Query focuses on caching, and Next.js integrates with its rendering model. The direction shown here covers core primitives most data systems need. Adapt based on your framework's server/client boundaries, caching strategy, and how you handle loading states.
实现说明: 以下模式和代码示例代表了构建数据获取系统的一种成熟方法。不同框架采用不同方法——Remix使用loaders,React Query专注于缓存,Next.js与自身渲染模型集成。此处展示的方向涵盖了大多数数据系统所需的核心原语。请根据框架的服务端/客户端边界、缓存策略以及加载状态处理方式进行调整。
Implementing Request Deduplication
实现请求去重
javascript
// REQUEST DEDUPLICATION IMPLEMENTATION
class RequestDeduplicator {
constructor() {
this.inflight = new Map();
this.cache = new Map();
}
async fetch(key, fetcher, options = {}) {
const { ttl = 0, forceRefresh = false } = options;
const keyStr = typeof key === 'string' ? key : JSON.stringify(key);
// Check cache first
if (!forceRefresh && this.cache.has(keyStr)) {
const cached = this.cache.get(keyStr);
if (Date.now() - cached.timestamp < ttl) {
return cached.data;
}
}
// Check if request is already in flight
if (this.inflight.has(keyStr)) {
return this.inflight.get(keyStr);
}
// Create new request
const promise = fetcher()
.then(data => {
this.cache.set(keyStr, { data, timestamp: Date.now() });
this.inflight.delete(keyStr);
return data;
})
.catch(error => {
this.inflight.delete(keyStr);
throw error;
});
this.inflight.set(keyStr, promise);
return promise;
}
invalidate(key) {
const keyStr = typeof key === 'string' ? key : JSON.stringify(key);
this.cache.delete(keyStr);
}
invalidateMatching(predicate) {
for (const key of this.cache.keys()) {
if (predicate(key)) {
this.cache.delete(key);
}
}
}
}
// Per-request deduplication (for SSR)
function createRequestScopedCache() {
const cache = new Map();
return function cachedFetch(url, options) {
const key = `${options?.method || 'GET'}:${url}`;
if (cache.has(key)) {
return cache.get(key);
}
const promise = fetch(url, options).then(r => r.json());
cache.set(key, promise);
return promise;
};
}javascript
// 请求去重实现
class RequestDeduplicator {
constructor() {
this.inflight = new Map();
this.cache = new Map();
}
async fetch(key, fetcher, options = {}) {
const { ttl = 0, forceRefresh = false } = options;
const keyStr = typeof key === 'string' ? key : JSON.stringify(key);
// 先检查缓存
if (!forceRefresh && this.cache.has(keyStr)) {
const cached = this.cache.get(keyStr);
if (Date.now() - cached.timestamp < ttl) {
return cached.data;
}
}
// 检查请求是否正在进行中
if (this.inflight.has(keyStr)) {
return this.inflight.get(keyStr);
}
// 创建新请求
const promise = fetcher()
.then(data => {
this.cache.set(keyStr, { data, timestamp: Date.now() });
this.inflight.delete(keyStr);
return data;
})
.catch(error => {
this.inflight.delete(keyStr);
throw error;
});
this.inflight.set(keyStr, promise);
return promise;
}
invalidate(key) {
const keyStr = typeof key === 'string' ? key : JSON.stringify(key);
this.cache.delete(keyStr);
}
invalidateMatching(predicate) {
for (const key of this.cache.keys()) {
if (predicate(key)) {
this.cache.delete(key);
}
}
}
}
// 每请求去重(适用于SSR)
function createRequestScopedCache() {
const cache = new Map();
return function cachedFetch(url, options) {
const key = `${options?.method || 'GET'}:${url}`;
if (cache.has(key)) {
return cache.get(key);
}
const promise = fetch(url, options).then(r => r.json());
cache.set(key, promise);
return promise;
};
}Building a Data Loader System
构建数据加载器系统
javascript
// DATA LOADER SYSTEM (Framework Integration)
class DataLoader {
constructor() {
this.loaders = new Map();
this.cache = new Map();
}
// Register a loader for a route
register(routeId, loader) {
this.loaders.set(routeId, loader);
}
// Load data for matched routes (parallel)
async loadRoute(matches, request) {
const results = await Promise.all(
matches.map(async (match) => {
const loader = this.loaders.get(match.route.id);
if (!loader) return { routeId: match.route.id, data: null };
const data = await loader({
params: match.params,
request,
context: {},
});
return { routeId: match.route.id, data };
})
);
// Return as map
return new Map(results.map(r => [r.routeId, r.data]));
}
// Parallel data loading with dependencies
async loadWithDeps(loaderGraph, request) {
const results = new Map();
const pending = new Set(loaderGraph.keys());
while (pending.size > 0) {
// Find loaders whose dependencies are satisfied
const ready = [...pending].filter(id => {
const deps = loaderGraph.get(id).dependsOn || [];
return deps.every(d => results.has(d));
});
if (ready.length === 0 && pending.size > 0) {
throw new Error('Circular dependency detected');
}
// Load in parallel
await Promise.all(
ready.map(async (id) => {
const { loader, dependsOn = [] } = loaderGraph.get(id);
const depData = Object.fromEntries(
dependsOn.map(d => [d, results.get(d)])
);
const data = await loader({ request, deps: depData });
results.set(id, data);
pending.delete(id);
})
);
}
return results;
}
}javascript
// 数据加载器系统(框架集成)
class DataLoader {
constructor() {
this.loaders = new Map();
this.cache = new Map();
}
// 为路由注册加载器
register(routeId, loader) {
this.loaders.set(routeId, loader);
}
// 为匹配的路由加载数据(并行)
async loadRoute(matches, request) {
const results = await Promise.all(
matches.map(async (match) => {
const loader = this.loaders.get(match.route.id);
if (!loader) return { routeId: match.route.id, data: null };
const data = await loader({
params: match.params,
request,
context: {},
});
return { routeId: match.route.id, data };
})
);
// 以Map形式返回
return new Map(results.map(r => [r.routeId, r.data]));
}
// 带依赖的并行数据加载
async loadWithDeps(loaderGraph, request) {
const results = new Map();
const pending = new Set(loaderGraph.keys());
while (pending.size > 0) {
// 找出依赖已满足的加载器
const ready = [...pending].filter(id => {
const deps = loaderGraph.get(id).dependsOn || [];
return deps.every(d => results.has(d));
});
if (ready.length === 0 && pending.size > 0) {
throw new Error('检测到循环依赖');
}
// 并行加载
await Promise.all(
ready.map(async (id) => {
const { loader, dependsOn = [] } = loaderGraph.get(id);
const depData = Object.fromEntries(
dependsOn.map(d => [d, results.get(d)])
);
const data = await loader({ request, deps: depData });
results.set(id, data);
pending.delete(id);
})
);
}
return results;
}
}Implementing Suspense-Compatible Data Fetching
实现支持Suspense的数据获取
javascript
// SUSPENSE DATA FETCHING
function createResource(fetcher) {
let status = 'pending';
let result;
const promise = fetcher()
.then(data => {
status = 'success';
result = data;
})
.catch(error => {
status = 'error';
result = error;
});
return {
read() {
switch (status) {
case 'pending':
throw promise; // Suspense catches this
case 'error':
throw result; // ErrorBoundary catches this
case 'success':
return result;
}
},
};
}
// Cache wrapper for resources
const resourceCache = new Map();
function getResource(key, fetcher) {
if (!resourceCache.has(key)) {
resourceCache.set(key, createResource(fetcher));
}
return resourceCache.get(key);
}
// Pre-load resources before rendering
function preloadResource(key, fetcher) {
if (!resourceCache.has(key)) {
resourceCache.set(key, createResource(fetcher));
}
}
// Clear resource cache
function invalidateResource(key) {
resourceCache.delete(key);
}
// Usage pattern
function UserProfile({ userId }) {
const resource = getResource(
['user', userId],
() => fetch(`/api/users/${userId}`).then(r => r.json())
);
// Will suspend until data is ready
const user = resource.read();
return <div>{user.name}</div>;
}javascript
// Suspense数据获取
function createResource(fetcher) {
let status = 'pending';
let result;
const promise = fetcher()
.then(data => {
status = 'success';
result = data;
})
.catch(error => {
status = 'error';
result = error;
});
return {
read() {
switch (status) {
case 'pending':
throw promise; // Suspense捕获此Promise
case 'error':
throw result; // ErrorBoundary捕获此错误
case 'success':
return result;
}
},
};
}
// 资源缓存包装器
const resourceCache = new Map();
function getResource(key, fetcher) {
if (!resourceCache.has(key)) {
resourceCache.set(key, createResource(fetcher));
}
return resourceCache.get(key);
}
// 渲染前预加载资源
function preloadResource(key, fetcher) {
if (!resourceCache.has(key)) {
resourceCache.set(key, createResource(fetcher));
}
}
// 清除资源缓存
function invalidateResource(key) {
resourceCache.delete(key);
}
// 使用模式
function UserProfile({ userId }) {
const resource = getResource(
['user', userId],
() => fetch(`/api/users/${userId}`).then(r => r.json())
);
// 数据未就绪时挂起
const user = resource.read();
return <div>{user.name}</div>;
}Streaming Data Implementation
流式数据实现
javascript
// STREAMING DATA LOADING
async function* streamJSON(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Try to parse complete JSON objects (newline-delimited)
const lines = buffer.split('\n');
buffer = lines.pop(); // Keep incomplete line
for (const line of lines) {
if (line.trim()) {
yield JSON.parse(line);
}
}
}
// Parse remaining buffer
if (buffer.trim()) {
yield JSON.parse(buffer);
}
}
// Server-Sent Events for real-time data
function createSSEConnection(url) {
const eventSource = new EventSource(url);
const listeners = new Set();
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
listeners.forEach(l => l(data));
};
return {
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
close() {
eventSource.close();
},
};
}
// React hook for streaming data
function useStreamingData(url) {
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function stream() {
for await (const item of streamJSON(url)) {
if (cancelled) break;
setItems(prev => [...prev, item]);
}
setIsLoading(false);
}
stream();
return () => { cancelled = true; };
}, [url]);
return { items, isLoading };
}javascript
// 流式数据加载
async function* streamJSON(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 尝试解析完整的JSON对象(换行分隔)
const lines = buffer.split('\n');
buffer = lines.pop(); // 保留不完整的行
for (const line of lines) {
if (line.trim()) {
yield JSON.parse(line);
}
}
}
// 解析剩余的缓冲区
if (buffer.trim()) {
yield JSON.parse(buffer);
}
}
// 服务端发送事件(用于实时数据)
function createSSEConnection(url) {
const eventSource = new EventSource(url);
const listeners = new Set();
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
listeners.forEach(l => l(data));
};
return {
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
close() {
eventSource.close();
},
};
}
// React流式数据钩子
function useStreamingData(url) {
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function stream() {
for await (const item of streamJSON(url)) {
if (cancelled) break;
setItems(prev => [...prev, item]);
}
setIsLoading(false);
}
stream();
return () => { cancelled = true; };
}, [url]);
return { items, isLoading };
}Building an Optimistic Update System
构建乐观更新系统
javascript
// OPTIMISTIC UPDATES IMPLEMENTATION
class OptimisticUpdateManager {
constructor(queryClient) {
this.queryClient = queryClient;
this.pendingUpdates = new Map();
}
async mutate(key, mutationFn, options = {}) {
const {
optimisticUpdate,
rollback = true,
invalidateKeys = [],
} = options;
const keyStr = JSON.stringify(key);
// Snapshot current state
const previousData = this.queryClient.getQueryData(key);
const mutationId = Date.now().toString();
try {
// Apply optimistic update
if (optimisticUpdate) {
const optimisticData = optimisticUpdate(previousData);
this.queryClient.setQueryData(key, optimisticData);
this.pendingUpdates.set(mutationId, { key, previousData });
}
// Execute actual mutation
const result = await mutationFn();
// Update with server result
if (result !== undefined) {
this.queryClient.setQueryData(key, result);
}
// Invalidate related queries
for (const invalidateKey of invalidateKeys) {
this.queryClient.invalidateQueries(invalidateKey);
}
this.pendingUpdates.delete(mutationId);
return result;
} catch (error) {
// Rollback on error
if (rollback && this.pendingUpdates.has(mutationId)) {
const { previousData } = this.pendingUpdates.get(mutationId);
this.queryClient.setQueryData(key, previousData);
this.pendingUpdates.delete(mutationId);
}
throw error;
}
}
// Batch multiple optimistic updates
async batchMutate(mutations) {
const snapshots = mutations.map(m => ({
key: m.key,
previous: this.queryClient.getQueryData(m.key),
}));
// Apply all optimistic updates
mutations.forEach((m, i) => {
if (m.optimisticUpdate) {
const data = m.optimisticUpdate(snapshots[i].previous);
this.queryClient.setQueryData(m.key, data);
}
});
try {
// Execute all mutations in parallel
const results = await Promise.all(
mutations.map(m => m.mutationFn())
);
return results;
} catch (error) {
// Rollback all
snapshots.forEach(s => {
this.queryClient.setQueryData(s.key, s.previous);
});
throw error;
}
}
}javascript
// 乐观更新实现
class OptimisticUpdateManager {
constructor(queryClient) {
this.queryClient = queryClient;
this.pendingUpdates = new Map();
}
async mutate(key, mutationFn, options = {}) {
const {
optimisticUpdate,
rollback = true,
invalidateKeys = [],
} = options;
const keyStr = JSON.stringify(key);
// 快照当前状态
const previousData = this.queryClient.getQueryData(key);
const mutationId = Date.now().toString();
try {
// 应用乐观更新
if (optimisticUpdate) {
const optimisticData = optimisticUpdate(previousData);
this.queryClient.setQueryData(key, optimisticData);
this.pendingUpdates.set(mutationId, { key, previousData });
}
// 执行实际变更
const result = await mutationFn();
// 用服务端结果更新
if (result !== undefined) {
this.queryClient.setQueryData(key, result);
}
// 失效相关查询
for (const invalidateKey of invalidateKeys) {
this.queryClient.invalidateQueries(invalidateKey);
}
this.pendingUpdates.delete(mutationId);
return result;
} catch (error) {
// 出错时回滚
if (rollback && this.pendingUpdates.has(mutationId)) {
const { previousData } = this.pendingUpdates.get(mutationId);
this.queryClient.setQueryData(key, previousData);
this.pendingUpdates.delete(mutationId);
}
throw error;
}
}
// 批量多个乐观更新
async batchMutate(mutations) {
const snapshots = mutations.map(m => ({
key: m.key,
previous: this.queryClient.getQueryData(m.key),
}));
// 应用所有乐观更新
mutations.forEach((m, i) => {
if (m.optimisticUpdate) {
const data = m.optimisticUpdate(snapshots[i].previous);
this.queryClient.setQueryData(m.key, data);
}
});
try {
// 并行执行所有变更
const results = await Promise.all(
mutations.map(m => m.mutationFn())
);
return results;
} catch (error) {
// 全部回滚
snapshots.forEach(s => {
this.queryClient.setQueryData(s.key, s.previous);
});
throw error;
}
}
}Cache Invalidation Strategies
缓存失效策略
javascript
// CACHE INVALIDATION PATTERNS
class CacheInvalidator {
constructor(cache) {
this.cache = cache;
this.tags = new Map(); // tag -> Set of keys
}
// Associate cache entry with tags
setWithTags(key, value, tags = []) {
this.cache.set(key, value);
for (const tag of tags) {
if (!this.tags.has(tag)) {
this.tags.set(tag, new Set());
}
this.tags.get(tag).add(key);
}
}
// Invalidate by exact key
invalidateKey(key) {
this.cache.delete(key);
}
// Invalidate by tag
invalidateTag(tag) {
const keys = this.tags.get(tag);
if (keys) {
for (const key of keys) {
this.cache.delete(key);
}
this.tags.delete(tag);
}
}
// Invalidate by pattern
invalidatePattern(pattern) {
const regex = new RegExp(pattern);
for (const key of this.cache.keys()) {
if (regex.test(key)) {
this.cache.delete(key);
}
}
}
// Time-based invalidation
setWithTTL(key, value, ttlMs) {
this.cache.set(key, {
value,
expiresAt: Date.now() + ttlMs,
});
// Schedule cleanup
setTimeout(() => this.cache.delete(key), ttlMs);
}
}
// Webhook-based invalidation (for ISR)
async function handleRevalidationWebhook(request) {
const { type, id, tags } = await request.json();
// Verify webhook signature
const signature = request.headers.get('x-webhook-signature');
if (!verifySignature(signature, request.body)) {
return new Response('Unauthorized', { status: 401 });
}
// Invalidate based on payload
if (id) {
await revalidatePath(`/${type}/${id}`);
}
if (tags) {
for (const tag of tags) {
await revalidateTag(tag);
}
}
return new Response('OK');
}javascript
// 缓存失效模式
class CacheInvalidator {
constructor(cache) {
this.cache = cache;
this.tags = new Map(); // 标签 -> 键集合
}
// 将缓存条目与标签关联
setWithTags(key, value, tags = []) {
this.cache.set(key, value);
for (const tag of tags) {
if (!this.tags.has(tag)) {
this.tags.set(tag, new Set());
}
this.tags.get(tag).add(key);
}
}
// 按精确键失效
invalidateKey(key) {
this.cache.delete(key);
}
// 按标签失效
invalidateTag(tag) {
const keys = this.tags.get(tag);
if (keys) {
for (const key of keys) {
this.cache.delete(key);
}
this.tags.delete(tag);
}
}
// 按模式失效
invalidatePattern(pattern) {
const regex = new RegExp(pattern);
for (const key of this.cache.keys()) {
if (regex.test(key)) {
this.cache.delete(key);
}
}
}
// 基于时间的失效
setWithTTL(key, value, ttlMs) {
this.cache.set(key, {
value,
expiresAt: Date.now() + ttlMs,
});
// 调度清理
setTimeout(() => this.cache.delete(key), ttlMs);
}
}
// Webhook-based失效(适用于ISR)
async function handleRevalidationWebhook(request) {
const { type, id, tags } = await request.json();
// 验证Webhook签名
const signature = request.headers.get('x-webhook-signature');
if (!verifySignature(signature, request.body)) {
return new Response('未授权', { status: 401 });
}
// 根据负载失效
if (id) {
await revalidatePath(`/${type}/${id}`);
}
if (tags) {
for (const tag of tags) {
await revalidateTag(tag);
}
}
return new Response('成功', { status: 200 });
}Related Skills
相关技能
- See rendering-patterns for SSR/SSG context
- See state-management-patterns for caching
- See hydration-patterns for client-side hydration
- 查看 rendering-patterns 了解SSR/SSG相关背景
- 查看 state-management-patterns 了解缓存相关内容
- 查看 hydration-patterns 了解客户端水合相关内容