关于 react-data-fetching
面向 React 开发者的数据获取模式指南,适用于多种应用框架。内容涵盖缓存、请求去重和同步策略,帮助改善加载流程。
主要功能
- 01并行执行独立请求
- 02使用 TanStack Query 或 SWR
- 03通过 Suspense 协调加载
- 04导航前预取数据
- 05去重服务端渲染请求
- 06实现乐观更新
适用场景
- 01替换 useEffect 中的 fetch
- 02减少 React 视图中的请求瀑布
- 03缓存并去重共享数据请求
- 04改进加载与数据变更反馈
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ patternsdev/ skills --skill react-data-fetching安装后在 Claude Code 中输入/react-data-fetching确认已加载。
详细介绍
React 数据获取模式
目录
适用于生产环境的 React 应用服务器端数据获取、缓存和同步模式。这些模式与框架无关——无论你使用 Vite + React Router、Next.js、Remix 还是自定义配置,都可以使用。
适用场景
在以下情况下参考这些模式:
- 为组件添加数据获取功能
- 将
useEffect+fetch替换为完善的数据层 - 实现缓存、请求去重或乐观更新
- 调试瀑布式加载模式
- 选择数据获取库
说明
- 在代码生成、审查和重构时应用这些模式。如果发现没有缓存或请求去重的 effect 内 fetch,请推荐合适的模式。
详情
概述
React 应用中最常见的性能问题是请求瀑布——本可并行运行的请求却按顺序执行。第二常见的问题是冗余请求——多个组件各自获取相同的数据。以下模式可解决这两类问题,并从影响最大的优化开始。
1. 使用 Promise.all 并行执行彼此独立的请求
影响:严重——消除顺序瀑布,可提升 2–10 倍性能。
当多个请求之间没有依赖关系时,应并发执行。
避免——顺序执行(3 次往返):
async function loadDashboard() {
const user = await fetchUser()
const posts = await fetchPosts()
const notifications = await fetchNotifications()
return { user, posts, notifications }
}
推荐——并行执行(1 次往返):
async function loadDashboard() {
const [user, posts, notifications] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchNotifications(),
])
return { user, posts, notifications }
}
如果请求之间存在部分依赖关系(B 依赖 A,但 C 不依赖 A),应立即启动独立的操作:
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. 等到需要使用值时再执行 Await
影响:高——尽早启动操作,不必阻塞等待暂时用不到的结果。
一个常见错误是立即对每个 promise 执行 await,即使后续代码暂时不需要其结果。应尽早启动 promise,然后在实际读取其值时再执行 await。
避免——造成不必要的阻塞:
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 }
}
推荐——尽早启动,稍后等待:
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 }
}
这与 Promise.all 互为补充:需要在请求之间获取中间结果时,使用延迟 await;可以一次性等待所有结果时,使用 Promise.all。
3. 使用 TanStack Query 获取客户端数据
影响:严重——自动缓存、请求去重、重新验证和错误处理。
原生的 useEffect + fetch 缺少缓存、请求去重、重试和后台刷新功能。请使用数据获取库。
避免——没有缓存、没有请求去重、没有错误处理:
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 应用):
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 是更轻量的替代方案,涵盖基本功能(请求去重、缓存、重新验证),但处理复杂变更工作流的功能较少。
两者都提供:请求去重、过期后重新验证缓存、自动重试和后台刷新。
Vite 应用的设置:
// 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>
)
4. 使用 Suspense 声明式管理加载状态
影响:高——代码更简洁、自动协调加载,并支持流式传输。
Suspense 允许你在组件树中声明加载边界,而不必在每个组件中管理 isLoading 状态。
避免——手动协调加载:
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 边界:
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 边界,以便它们各自加载:
function Dashboard() {
return (
<div>
<Suspense fallback={<HeaderSkeleton />}>
<UserHeader />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<StatsPanel />
</Suspense>
</div>
)
}
TanStack Query 提供 useSuspenseQuery,SWR 则提供 { suspense: true } 选项。
5. 在导航前预取数据
影响:高——消除页面切换时的加载状态。
在用户确认导航前开始获取数据——例如在悬停、聚焦或路由预加载时。
使用 TanStack Query:
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 应用):
// routes.tsx
const routes = [
{
path: '/projects/:id',
loader: ({ params }) => queryClient.ensureQueryData({
queryKey: ['project', params.id],
queryFn: () => fetchProject(params.id!),
}),
Component: ProjectPage,
},
]
6. 使用 React.cache() 在服务端去重
影响:中等 — 在单次服务端渲染期间对开销较大的操作去重。
在服务端组件(RSC)中,React.cache() 可确保多个组件发起的相同异步调用在每个请求中只执行一次。
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() 时,会共享一次执行。
重要: 缓存键应使用基本类型参数(字符串、数字)。内联对象会创建新的引用,导致缓存未命中:
// Cache miss every time — new object reference
getUser({ id: '123' })
getUser({ id: '123' }) // miss
// Cache hit — same string value
getUser('123')
getUser('123') // hit
7. 实现乐观更新,即时反馈
影响:高 — 无需等待服务端响应,UI 即可立即更新。
对于结果可预测的变更(例如切换点赞状态、更新名称),应立即更新 UI,再根据服务端响应进行协调。
使用 TanStack Query:
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. 避免组件树中的请求瀑布
影响:严重 — 先请求父组件数据、再请求子组件数据是首要的性能问题。
如果父组件先请求数据,而子组件又基于父组件的结果请求自己的数据,就会形成请求瀑布。应调整结构,以便并行请求。
避免 — 子组件必须等父组件完成后才能开始请求:
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),
})
// ...
}
推荐 — 在同一层级同时请求:
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. 对全局事件监听器去重
影响:中等 — 避免 N 个组件实例创建 N 个监听器。
当多个组件实例需要监听相同的全局事件(缩放、滚动、联网状态)时,应共享单个监听器。
// 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. 为滚动和触摸事件使用被动监听器
影响:低至中等 — 避免监听器阻塞滚动,造成卡顿。
非被动的滚动/触摸监听器会阻塞浏览器的合成线程。如果不调用 preventDefault(),应将监听器标记为被动。
避免 — 会阻塞滚动:
useEffect(() => {
const handler = () => trackScroll(window.scrollY)
window.addEventListener('scroll', handler)
return () => window.removeEventListener('scroll', handler)
}, [])
推荐 — 不会阻塞:
useEffect(() => {
const handler = () => trackScroll(window.scrollY)
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
11. 为客户端存储设置模式版本
影响:低至中等 — 避免过期的 localStorage 数据导致崩溃。
从 localStorage 或 sessionStorage 读取数据时,先前应用版本留下的过期数据可能导致应用崩溃。应添加模式版本并进行验证。
避免 — 模式变更时会崩溃:
const [prefs, setPrefs] = useState(() => {
return JSON.parse(localStorage.getItem('prefs') || '{}')
})
推荐 — 添加版本并提供回退方案:
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])
来源
相关模式来自 patterns.dev——面向更广泛的 Web 工程社区、与框架无关的 React 数据获取指南。