Loading...
Loading...
Compare original and translation side by side
| 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测试 |
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
if (!user) return <Loading />;
return <Profile user={user} />;
}Component renders → useEffect runs → Fetch starts → Data arrives → Re-render
[-- Waiting --] [-- Display --]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)function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
if (!user) return <Loading />;
return <Profile user={user} />;
}组件渲染 → useEffect执行 → 请求开始 → 数据到达 → 重新渲染
[-- 等待中 --] [-- 展示内容 --]function Dashboard() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(setUser); // 请求1
}, []);
if (!user) return <Loading />;
return (
<UserPosts userId={user.id} /> // 请求2需等待请求1完成后才能开始
);
}
// 时间线:
// [获取用户数据]───────►[获取帖子数据]───────►展示内容
// ↑ 必须等用户数据加载完成才能开始(瀑布流)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} />
</>
);
}[Fetch User ]
[Fetch Posts ]──►All Complete───►Render All
[Fetch Stats ]
↑ Parallel, but wait for slowestfunction 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} />
</>
);
}[获取用户数据 ]
[获取帖子数据 ]──►全部完成───►渲染所有组件
[获取统计数据 ]
↑ 并行执行,但需等待最慢的请求// 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>;
}[Fetch User ]───►Render User
[Fetch Posts ]─────────►Render Posts
↑ Independent, progressive rendering// 立即开始请求(不在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>;
}[获取用户数据 ]───►渲染用户组件
[获取帖子数据 ]─────────►渲染帖子组件
↑ 独立执行,渐进式渲染// Next.js App Router
async function ProductPage({ params }) {
const product = await db.products.findById(params.id); // Runs on server
return <ProductDetails product={product} />;
}// Next.js App Router
async function ProductPage({ params }) {
const product = await db.products.findById(params.id); // 在服务端执行
return <ProductDetails product={product} />;
}// 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);// 错误示例:串行执行(瀑布流)
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);<Suspense><Loading />// 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>
);
}undefined<Suspense><Loading />// 示例:立即就绪和加载缓慢的组件乱序流式传输
async function Page() {
return (
<div>
<Header />
{/* <Header>立即流式传输到客户端 */}
<Suspense fallback={<Loading />}>
<SlowComponent />
{/* 显示占位符<Loading />,当服务端流式传输完成后,客户端JS
会将其替换为<SlowComponent>的内容 */}
</Suspense>
<Footer />
{/* <Footer>可立即流式传输,
可能在<SlowComponent>之前或之后,取决于哪个先完成 */}
</div>
);
}undefined// 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// 无请求去重
// 组件A: fetch('/api/user')
// 组件B: fetch('/api/user')
// = 2次请求
// 有请求去重(React cache / TanStack Query)
// 组件A: useQuery(['user'], fetchUser)
// 组件B: useQuery(['user'], fetchUser)
// = 1次请求,共享结果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 arrivesconst { data } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
staleTime: 60000, // 60秒内视为新鲜数据
refetchOnMount: true, // 后台重新获取
});
// 时间线:
// 1. 立即显示缓存数据
// 2. 检查数据是否过期
// 3. 如果过期,在后台重新获取
// 4. 新鲜数据到达后更新UIconst mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries(['posts']);
// Or update cache directly
queryClient.setQueryData(['posts'], (old) => [...old, newPost]);
},
});const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
// 失效并重新获取
queryClient.invalidateQueries(['posts']);
// 或直接更新缓存
queryClient.setQueryData(['posts'], (old) => [...old, newPost]);
},
});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>
);
}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>
);
}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);
},
});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);
},
});const { data } = useQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id),
placeholderData: {
name: 'Loading...',
price: '--',
image: '/placeholder.jpg',
},
});const { data } = useQuery({
queryKey: ['product', id],
queryFn: () => fetchProduct(id),
placeholderData: {
name: '加载中...',
price: '--',
image: '/placeholder.jpg',
},
});<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</ErrorBoundary><ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</ErrorBoundary>const { data } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
retry: 3, // Retry 3 times
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000), // Exponential backoff
});const { data } = useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
retry: 3, // 重试3次
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000), // 指数退避
});| 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 | 最快 | 定期更新 | 中 |
| 流式传输 | 渐进式 | 请求时新鲜 | 中 |
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 + 客户端水合
└── 否 → 客户端获取// 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]);// 错误示例:可能存在竞态条件
useEffect(() => {
fetchData().then(setData);
}, [id]);
// 正确示例:清理或使用库
useEffect(() => {
let cancelled = false;
fetchData().then(data => {
if (!cancelled) setData(data);
});
return () => { cancelled = true; };
}, [id]);// 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} />;// 错误示例:未处理加载或错误状态
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} />;// 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
);// 错误示例:获取超出需求的数据
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个字段
);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 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-910ms// 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)// 瀑布流代码:
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)// 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 time// 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// 场景:多个组件需要用户数据
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,浪费带宽和时间// 使用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次网络请求,在组件间共享// 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);
}// 问题:数据变更后缓存过期
// 用户编辑个人资料
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);
}// 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
});
}// 错误示例:
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自动取消过时请求
});
}// 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>
);
}// 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// 传统方式:每个组件处理加载逻辑
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>
);
}// 概念性机制:
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)
// - 服务端组件// 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!// 传统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: 评论流式传输完成
// 用户渐进式看到内容!// 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// 无乐观更新:
// 用户点击"点赞"
// 加载图标显示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%的交互可能需要回滚// 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// 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 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>// 错误类型:
// 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>// 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'));// 简单实现:
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'));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与自身渲染模型集成。此处展示的方向涵盖了大多数数据系统所需的核心原语。请根据框架的服务端/客户端边界、缓存策略以及加载状态处理方式进行调整。
// 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;
};
}// 请求去重实现
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;
};
}// 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;
}
}// 数据加载器系统(框架集成)
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;
}
}// 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>;
}// 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 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 };
}// 流式数据加载
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 };
}// 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;
}
}
}// 乐观更新实现
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 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');
}// 缓存失效模式
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 });
}