react-data-fetching
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact Data Fetching Patterns
React数据获取模式
Production-ready patterns for fetching, caching, and synchronizing server data in React applications. These patterns are framework-agnostic — they work whether you're using Vite + React Router, Next.js, Remix, or a custom setup.
适用于React应用的生产级数据获取、缓存和服务器数据同步模式。这些模式与框架无关——无论你使用Vite + React Router、Next.js、Remix还是自定义配置都能适用。
When to Use
适用场景
Reference these patterns when:
- Adding data fetching to components
- Replacing +
useEffectwith a proper data layerfetch - Implementing caching, deduplication, or optimistic updates
- Debugging waterfall loading patterns
- Choosing between data fetching libraries
在以下场景中参考这些模式:
- 为组件添加数据获取功能
- 使用合适的数据层替代+
useEffect组合fetch - 实现缓存、请求去重或乐观更新
- 调试瀑布式加载问题
- 选择数据获取库
Instructions
使用说明
- Apply these patterns during code generation, review, and refactoring. When you see fetch-in-effect without caching or deduplication, suggest the appropriate pattern.
- 在代码生成、评审和重构阶段应用这些模式。当你看到在useEffect中直接使用fetch且未实现缓存或去重时,建议采用合适的模式。
Details
详细内容
Overview
概述
The most common performance problem in React apps is request waterfalls — sequential fetches that could run in parallel. The second most common problem is redundant fetches — multiple components fetching the same data independently. The patterns below address both, starting with the highest-impact fixes.
React应用中最常见的性能问题是请求瀑布流——原本可以并行执行的请求被串行处理。第二常见的问题是重复请求——多个组件独立获取相同的数据。以下模式可以解决这两个问题,从影响最大的修复方案开始介绍。
1. Parallelize Independent Fetches with Promise.all
Promise.all1. 使用Promise.all
并行执行独立请求
Promise.allImpact: CRITICAL — Eliminates sequential waterfalls for 2-10x improvement.
When multiple fetches have no dependencies on each other, run them concurrently.
Avoid — sequential (3 round trips):
typescript
async function loadDashboard() {
const user = await fetchUser()
const posts = await fetchPosts()
const notifications = await fetchNotifications()
return { user, posts, notifications }
}Prefer — parallel (1 round trip):
typescript
async function loadDashboard() {
const [user, posts, notifications] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchNotifications(),
])
return { user, posts, notifications }
}When fetches have partial dependencies (B depends on A, but C doesn't), start independent work immediately:
typescript
async function loadPage() {
const userPromise = fetchUser()
const configPromise = fetchConfig()
const user = await userPromise
const [config, posts] = await Promise.all([
configPromise,
fetchPosts(user.id), // depends on user
])
return { user, config, posts }
}影响:关键——消除串行瀑布流,性能提升2-10倍。
当多个请求之间没有依赖关系时,并行执行它们。
避免——串行执行(3次往返):
typescript
async function loadDashboard() {
const user = await fetchUser()
const posts = await fetchPosts()
const notifications = await fetchNotifications()
return { user, posts, notifications }
}推荐——并行执行(1次往返):
typescript
async function loadDashboard() {
const [user, posts, notifications] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchNotifications(),
])
return { user, posts, notifications }
}当请求存在部分依赖(B依赖A,但C不依赖)时,立即启动独立请求:
typescript
async function loadPage() {
const userPromise = fetchUser()
const configPromise = fetchConfig()
const user = await userPromise
const [config, posts] = await Promise.all([
configPromise,
fetchPosts(user.id), // 依赖user数据
])
return { user, config, posts }
}2. Defer Await Until the Value Is Needed
2. 延迟await
直到需要数据时
awaitImpact: HIGH — Starts work earlier without blocking on results you don't need yet.
A common mistake is to each promise immediately, even when subsequent code doesn't need the result right away. Start the promise early, then it at the point where you actually read the value.
awaitawaitAvoid — blocks unnecessarily:
typescript
async function loadProfile(userId: string) {
const user = await fetchUser(userId) // waits here
const prefs = await fetchPreferences() // starts only after user resolves
const avatar = buildAvatarUrl(user.avatar)
return { user, prefs, avatar }
}Prefer — start early, await late:
typescript
async function loadProfile(userId: string) {
const userPromise = fetchUser(userId) // starts immediately
const prefsPromise = fetchPreferences() // starts immediately
const user = await userPromise // await when needed
const avatar = buildAvatarUrl(user.avatar)
const prefs = await prefsPromise // may already be resolved
return { user, prefs, avatar }
}This is complementary to — use defer-await when you need intermediate results between fetches, and when you can wait for everything at once.
Promise.allPromise.all影响:高——提前启动请求,不会被暂时不需要的结果阻塞。
常见错误是立即每个Promise,即使后续代码暂时不需要该结果。提前启动Promise,在实际需要读取值时再。
awaitawait避免——不必要的阻塞:
typescript
async function loadProfile(userId: string) {
const user = await fetchUser(userId) // 在此处等待
const prefs = await fetchPreferences() // 仅在user请求完成后启动
const avatar = buildAvatarUrl(user.avatar)
return { user, prefs, avatar }
}推荐——提前启动,延迟等待:
typescript
async function loadProfile(userId: string) {
const userPromise = fetchUser(userId) // 立即启动
const prefsPromise = fetchPreferences() // 立即启动
const user = await userPromise // 需要时再等待
const avatar = buildAvatarUrl(user.avatar)
const prefs = await prefsPromise // 可能已完成
return { user, prefs, avatar }
}这与互补——当你需要在请求之间获取中间结果时使用延迟等待,当可以一次性等待所有结果时使用。
Promise.allPromise.all3. Use TanStack Query for Client-Side Data
3. 使用TanStack Query处理客户端数据
Impact: CRITICAL — Automatic caching, deduplication, revalidation, and error handling.
Raw + lacks caching, deduplication, retry, and background refresh. Use a data fetching library.
useEffectfetchAvoid — no caching, no dedup, no error handling:
tsx
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(true)
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser)
.finally(() => setLoading(false))
}, [userId])
if (loading) return <Skeleton />
return <div>{user?.name}</div>
}Prefer — TanStack Query (recommended for Vite + React apps):
tsx
import { useQuery } from '@tanstack/react-query'
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
})
if (isLoading) return <Skeleton />
return <div>{user?.name}</div>
}TanStack Query is the strongest choice for Vite apps — it's framework-agnostic, has built-in , devtools, infinite queries, optimistic mutations, and offline support. SWR is a lighter alternative that covers the basics (dedup, caching, revalidation) but has fewer features for complex mutation workflows.
useSuspenseQueryBoth give you: request deduplication, stale-while-revalidate caching, automatic retries, and background refresh.
Setup for Vite apps:
tsx
// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000, // 1 minute
retry: 2,
},
},
})
createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
)影响:关键——自动实现缓存、请求去重、重新验证和错误处理。
原生 + 缺乏缓存、去重、重试和后台刷新功能。使用数据获取库来解决这些问题。
useEffectfetch避免——无缓存、无去重、无错误处理:
tsx
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
setLoading(true)
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser)
.finally(() => setLoading(false))
}, [userId])
if (loading) return <Skeleton />
return <div>{user?.name}</div>
}推荐——TanStack Query(推荐用于Vite + React应用):
tsx
import { useQuery } from '@tanstack/react-query'
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
})
if (isLoading) return <Skeleton />
return <div>{user?.name}</div>
}TanStack Query是Vite应用的最佳选择——它与框架无关,内置、开发工具、无限查询、乐观更新和离线支持。SWR是更轻量的替代方案,涵盖基础功能(去重、缓存、重新验证),但在复杂更新流程方面功能较少。
useSuspenseQuery两者都能提供:请求去重、stale-while-revalidate缓存、自动重试和后台刷新。
Vite应用配置:
tsx
// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000, // 1分钟
retry: 2,
},
},
})
createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
)4. Use Suspense for Declarative Loading States
4. 使用Suspense实现声明式加载状态
Impact: HIGH — Cleaner code, automatic loading coordination, streaming support.
Suspense lets you declare loading boundaries in the component tree instead of managing state in every component.
isLoadingAvoid — manual loading orchestration:
tsx
function Dashboard() {
const { data: user, isLoading: userLoading } = useQuery(userQuery)
const { data: stats, isLoading: statsLoading } = useQuery(statsQuery)
if (userLoading || statsLoading) return <FullPageSpinner />
return (
<div>
<UserHeader user={user} />
<StatsPanel stats={stats} />
</div>
)
}Prefer — Suspense boundaries:
tsx
function Dashboard() {
return (
<Suspense fallback={<FullPageSpinner />}>
<DashboardContent />
</Suspense>
)
}
function DashboardContent() {
const { data: user } = useSuspenseQuery(userQuery)
const { data: stats } = useSuspenseQuery(statsQuery)
return (
<div>
<UserHeader user={user} />
<StatsPanel stats={stats} />
</div>
)
}For independent sections, use separate Suspense boundaries so they load independently:
tsx
function Dashboard() {
return (
<div>
<Suspense fallback={<HeaderSkeleton />}>
<UserHeader />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel />
</Suspense>
</div>
)
}TanStack Query provides and SWR provides option.
useSuspenseQuery{ suspense: true }影响:高——代码更简洁,自动协调加载状态,支持流式传输。
Suspense允许你在组件树中声明加载边界,而不是在每个组件中管理状态。
isLoading避免——手动管理加载流程:
tsx
function Dashboard() {
const { data: user, isLoading: userLoading } = useQuery(userQuery)
const { data: stats, isLoading: statsLoading } = useQuery(statsQuery)
if (userLoading || statsLoading) return <FullPageSpinner />
return (
<div>
<UserHeader user={user} />
<StatsPanel stats={stats} />
</div>
)
}推荐——Suspense边界:
tsx
function Dashboard() {
return (
<Suspense fallback={<FullPageSpinner />}>
<DashboardContent />
</Suspense>
)
}
function DashboardContent() {
const { data: user } = useSuspenseQuery(userQuery)
const { data: stats } = useSuspenseQuery(statsQuery)
return (
<div>
<UserHeader user={user} />
<StatsPanel stats={stats} />
</div>
)
}对于独立模块,使用单独的Suspense边界,使其可以独立加载:
tsx
function Dashboard() {
return (
<div>
<Suspense fallback={<HeaderSkeleton />}>
<UserHeader />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel />
</Suspense>
</div>
)
}TanStack Query提供,SWR提供选项。
useSuspenseQuery{ suspense: true }5. Prefetch Data Before Navigation
5. 导航前预获取数据
Impact: HIGH — Eliminates loading states on page transitions.
Start fetching data before the user commits to a navigation — on hover, focus, or route preload.
With TanStack Query:
tsx
import { useQueryClient } from '@tanstack/react-query'
function ProjectLink({ projectId }: { projectId: string }) {
const queryClient = useQueryClient()
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: ['project', projectId],
queryFn: () => fetchProject(projectId),
staleTime: 30_000,
})
}
return (
<Link
to={`/projects/${projectId}`}
onMouseEnter={prefetch}
onFocus={prefetch}
>
View Project
</Link>
)
}With React Router loaders (Vite apps):
tsx
// routes.tsx
const routes = [
{
path: '/projects/:id',
loader: ({ params }) => queryClient.ensureQueryData({
queryKey: ['project', params.id],
queryFn: () => fetchProject(params.id!),
}),
Component: ProjectPage,
},
]影响:高——消除页面切换时的加载状态。
在用户确认导航前就开始获取数据——比如在鼠标悬停、聚焦或路由预加载时。
使用TanStack Query:
tsx
import { useQueryClient } from '@tanstack/react-query'
function ProjectLink({ projectId }: { projectId: string }) {
const queryClient = useQueryClient()
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: ['project', projectId],
queryFn: () => fetchProject(projectId),
staleTime: 30_000,
})
}
return (
<Link
to={`/projects/${projectId}`}
onMouseEnter={prefetch}
onFocus={prefetch}
>
View Project
</Link>
)
}使用React Router加载器(Vite应用):
tsx
// routes.tsx
const routes = [
{
path: '/projects/:id',
loader: ({ params }) => queryClient.ensureQueryData({
queryKey: ['project', params.id],
queryFn: () => fetchProject(params.id!),
}),
Component: ProjectPage,
},
]6. Use React.cache()
for Server-Side Deduplication
React.cache()6. 使用React.cache()
实现服务端去重
React.cache()Impact: MEDIUM — Deduplicates expensive operations within a single server render.
In server components (RSC), ensures the same async call made by multiple components only executes once per request.
React.cache()typescript
import { cache } from 'react'
export const getSession = cache(async () => {
const session = await auth()
if (!session?.user?.id) return null
return session
})
export const getUser = cache(async (userId: string) => {
return await db.user.findUnique({ where: { id: userId } })
})Multiple components calling in the same render share one execution.
getSession()Important: Use primitive arguments (strings, numbers) for cache keys. Inline objects create new references and cause cache misses:
typescript
// Cache miss every time — new object reference
getUser({ id: '123' })
getUser({ id: '123' }) // miss
// Cache hit — same string value
getUser('123')
getUser('123') // hit影响:中——在单次服务端渲染中去重昂贵的操作。
在服务端组件(RSC)中,确保多个组件调用的同一个异步操作在单次请求中只执行一次。
React.cache()typescript
import { cache } from 'react'
export const getSession = cache(async () => {
const session = await auth()
if (!session?.user?.id) return null
return session
})
export const getUser = cache(async (userId: string) => {
return await db.user.findUnique({ where: { id: userId } })
})多个组件在同一次渲染中调用会共享同一个执行结果。
getSession()注意: 使用原始类型参数(字符串、数字)作为缓存键。内联对象会创建新的引用,导致缓存未命中:
typescript
// 每次都缓存未命中——新对象引用
getUser({ id: '123' })
getUser({ id: '123' }) // 未命中
// 缓存命中——相同字符串值
getUser('123')
getUser('123') // 命中7. Implement Optimistic Updates for Instant Feedback
7. 实现乐观更新以获得即时反馈
Impact: HIGH — UI responds immediately without waiting for the server.
For mutations where the outcome is predictable (toggling a like, updating a name), update the UI instantly and reconcile with the server response.
With TanStack Query:
tsx
import { useMutation, useQueryClient } from '@tanstack/react-query'
function LikeButton({ postId }: { postId: string }) {
const queryClient = useQueryClient()
const { mutate: toggleLike } = useMutation({
mutationFn: () => fetch(`/api/posts/${postId}/like`, { method: 'POST' }),
onMutate: async () => {
await queryClient.cancelQueries({ queryKey: ['post', postId] })
const previous = queryClient.getQueryData<Post>(['post', postId])
queryClient.setQueryData<Post>(['post', postId], old => ({
...old!,
liked: !old!.liked,
likeCount: old!.liked ? old!.likeCount - 1 : old!.likeCount + 1,
}))
return { previous }
},
onError: (_err, _vars, context) => {
queryClient.setQueryData(['post', postId], context?.previous)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['post', postId] })
},
})
return <button onClick={() => toggleLike()}>Like</button>
}影响:高——UI立即响应,无需等待服务器返回结果。
对于结果可预测的更新操作(比如点赞、修改名称),先即时更新UI,再与服务器响应进行同步。
使用TanStack Query:
tsx
import { useMutation, useQueryClient } from '@tanstack/react-query'
function LikeButton({ postId }: { postId: string }) {
const queryClient = useQueryClient()
const { mutate: toggleLike } = useMutation({
mutationFn: () => fetch(`/api/posts/${postId}/like`, { method: 'POST' }),
onMutate: async () => {
await queryClient.cancelQueries({ queryKey: ['post', postId] })
const previous = queryClient.getQueryData<Post>(['post', postId])
queryClient.setQueryData<Post>(['post', postId], old => ({
...old!,
liked: !old!.liked,
likeCount: old!.liked ? old!.likeCount - 1 : old!.likeCount + 1,
}))
return { previous }
},
onError: (_err, _vars, context) => {
queryClient.setQueryData(['post', postId], context?.previous)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['post', postId] })
},
})
return <button onClick={() => toggleLike()}>Like</button>
}8. Avoid Fetch Waterfalls in Component Trees
8. 避免组件树中的请求瀑布流
Impact: CRITICAL — Parent-then-child fetching is the #1 performance problem.
When a parent fetches data and a child fetches its own data based on the parent's result, you create a waterfall. Restructure to fetch in parallel.
Avoid — child can't start until parent finishes:
tsx
function UserPage({ userId }: { userId: string }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
if (!user) return <Skeleton />
return <UserPosts userId={user.id} /> // starts fetching only after user loads
}
function UserPosts({ userId }: { userId: string }) {
const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPosts(userId),
})
// ...
}Prefer — fetch both at the same level:
tsx
function UserPage({ userId }: { userId: string }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPosts(userId),
})
if (!user) return <Skeleton />
return (
<div>
<UserHeader user={user} />
<PostList posts={posts ?? []} />
</div>
)
}Or use a route-level loader to fetch all data before the component renders.
影响:关键——父组件先获取数据,子组件再基于父组件结果获取数据是排名第一的性能问题。
当父组件获取数据,子组件基于父组件的结果获取自己的数据时,就会产生瀑布流。重构代码以并行获取数据。
避免——子组件需等待父组件完成:
tsx
function UserPage({ userId }: { userId: string }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
if (!user) return <Skeleton />
return <UserPosts userId={user.id} /> // 仅在user加载完成后开始获取
}
function UserPosts({ userId }: { userId: string }) {
const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPosts(userId),
})
// ...
}推荐——在同一层级并行获取:
tsx
function UserPage({ userId }: { userId: string }) {
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPosts(userId),
})
if (!user) return <Skeleton />
return (
<div>
<UserHeader user={user} />
<PostList posts={posts ?? []} />
</div>
)
}或者使用路由级别的加载器,在组件渲染前获取所有数据。
9. Deduplicate Global Event Listeners
9. 去重全局事件监听器
Impact: MEDIUM — Prevents N listeners for N component instances.
When multiple component instances need the same global event (resize, scroll, online), share a single listener.
typescript
// hooks/useOnlineStatus.ts
import { useSyncExternalStore } from 'react'
function subscribe(callback: () => void) {
window.addEventListener('online', callback)
window.addEventListener('offline', callback)
return () => {
window.removeEventListener('online', callback)
window.removeEventListener('offline', callback)
}
}
function getSnapshot() {
return navigator.onLine
}
export function useOnlineStatus() {
return useSyncExternalStore(subscribe, getSnapshot, () => true)
}useSyncExternalStore影响:中——避免N个组件实例创建N个监听器。
当多个组件实例需要监听同一个全局事件(resize、scroll、online)时,共享单个监听器。
typescript
// hooks/useOnlineStatus.ts
import { useSyncExternalStore } from 'react'
function subscribe(callback: () => void) {
window.addEventListener('online', callback)
window.addEventListener('offline', callback)
return () => {
window.removeEventListener('online', callback)
window.removeEventListener('offline', callback)
}
}
function getSnapshot() {
return navigator.onLine
}
export function useOnlineStatus() {
return useSyncExternalStore(subscribe, getSnapshot, () => true)
}useSyncExternalStore10. Use Passive Event Listeners for Scroll and Touch
10. 为滚动和触摸事件使用被动监听器
Impact: LOW-MEDIUM — Prevents scroll jank from blocking listeners.
Non-passive scroll/touch listeners block the browser's compositor thread. Always mark them passive when you don't call .
preventDefault()Avoid — blocks scrolling:
tsx
useEffect(() => {
const handler = () => trackScroll(window.scrollY)
window.addEventListener('scroll', handler)
return () => window.removeEventListener('scroll', handler)
}, [])Prefer — non-blocking:
tsx
useEffect(() => {
const handler = () => trackScroll(window.scrollY)
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])影响:低-中——避免监听器阻塞导致滚动卡顿。
非被动的滚动/触摸监听器会阻塞浏览器的合成线程。当你不需要调用时,始终将它们标记为被动。
preventDefault()避免——阻塞滚动:
tsx
useEffect(() => {
const handler = () => trackScroll(window.scrollY)
window.addEventListener('scroll', handler)
return () => window.removeEventListener('scroll', handler)
}, [])推荐——非阻塞:
tsx
useEffect(() => {
const handler = () => trackScroll(window.scrollY)
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])11. Schema-Version Your Client Storage
11. 为客户端存储添加版本标识
Impact: LOW-MEDIUM — Prevents crashes from stale localStorage data.
When reading from localStorage or sessionStorage, stale data from a previous app version can crash your app. Add a schema version and validate.
Avoid — crashes on schema change:
tsx
const [prefs, setPrefs] = useState(() => {
return JSON.parse(localStorage.getItem('prefs') || '{}')
})Prefer — versioned with fallback:
tsx
const PREFS_VERSION = 2
const [prefs, setPrefs] = useState<Prefs>(() => {
try {
const raw = localStorage.getItem('prefs')
if (!raw) return DEFAULT_PREFS
const parsed = JSON.parse(raw)
if (parsed._v !== PREFS_VERSION) return DEFAULT_PREFS
return parsed
} catch {
return DEFAULT_PREFS
}
})
// On save, include version
useEffect(() => {
localStorage.setItem('prefs', JSON.stringify({ ...prefs, _v: PREFS_VERSION }))
}, [prefs])影响:低-中——避免过时的localStorage数据导致崩溃。
当从localStorage或sessionStorage读取数据时,旧版本应用的过时数据可能导致应用崩溃。添加版本标识并进行验证。
避免——架构变更时崩溃:
tsx
const [prefs, setPrefs] = useState(() => {
return JSON.parse(localStorage.getItem('prefs') || '{}')
})推荐——带版本和回退:
tsx
const PREFS_VERSION = 2
const [prefs, setPrefs] = useState<Prefs>(() => {
try {
const raw = localStorage.getItem('prefs')
if (!raw) return DEFAULT_PREFS
const parsed = JSON.parse(raw)
if (parsed._v !== PREFS_VERSION) return DEFAULT_PREFS
return parsed
} catch {
return DEFAULT_PREFS
}
})
// 保存时包含版本
useEffect(() => {
localStorage.setItem('prefs', JSON.stringify({ ...prefs, _v: PREFS_VERSION }))
}, [prefs])Source
来源
Patterns from patterns.dev — framework-agnostic React data fetching guidance for the broader web engineering community.
这些模式来自patterns.dev——面向广大Web工程师社区的与框架无关的React数据获取指南。