react-data-fetching

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React 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
    useEffect
    +
    fetch
    with a proper data layer
  • 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

1. 使用
Promise.all
并行执行独立请求

Impact: 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
直到需要数据时

Impact: HIGH — Starts work earlier without blocking on results you don't need yet.
A common mistake is to
await
each promise immediately, even when subsequent code doesn't need the result right away. Start the promise early, then
await
it at the point where you actually read the value.
Avoid — 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
Promise.all
— use defer-await when you need intermediate results between fetches, and
Promise.all
when you can wait for everything at once.

影响:高——提前启动请求,不会被暂时不需要的结果阻塞。
常见错误是立即
await
每个Promise,即使后续代码暂时不需要该结果。提前启动Promise,在实际需要读取值时再
await
避免——不必要的阻塞:
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.all
互补——当你需要在请求之间获取中间结果时使用延迟等待,当可以一次性等待所有结果时使用
Promise.all

3. Use TanStack Query for Client-Side Data

3. 使用TanStack Query处理客户端数据

Impact: CRITICAL — Automatic caching, deduplication, revalidation, and error handling.
Raw
useEffect
+
fetch
lacks caching, deduplication, retry, and background refresh. Use a data fetching library.
Avoid — 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
useSuspenseQuery
, 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.
Both 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>
)

影响:关键——自动实现缓存、请求去重、重新验证和错误处理。
原生
useEffect
+
fetch
缺乏缓存、去重、重试和后台刷新功能。使用数据获取库来解决这些问题。
避免——无缓存、无去重、无错误处理:
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应用的最佳选择——它与框架无关,内置
useSuspenseQuery
、开发工具、无限查询、乐观更新和离线支持。SWR是更轻量的替代方案,涵盖基础功能(去重、缓存、重新验证),但在复杂更新流程方面功能较少。
两者都能提供:请求去重、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
isLoading
state in every component.
Avoid — 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
useSuspenseQuery
and SWR provides
{ suspense: true }
option.

影响:高——代码更简洁,自动协调加载状态,支持流式传输。
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提供
useSuspenseQuery
,SWR提供
{ 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

6. 使用
React.cache()
实现服务端去重

Impact: MEDIUM — Deduplicates expensive operations within a single server render.
In server components (RSC),
React.cache()
ensures the same async call made by multiple components only executes once per request.
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
getSession()
in the same render share one execution.
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
automatically deduplicates subscriptions and ensures consistent state across concurrent renders.

影响:中——避免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)
}
useSyncExternalStore
会自动去重订阅,并确保在并发渲染中状态一致。

10. 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数据获取指南。