react-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Patterns

React模式

Performance and composition patterns for React 19 + Vite + Cloudflare Workers projects. Use as a checklist when writing new components, a review guide when auditing existing code, or a refactoring playbook when something feels slow or tangled.
Rules are ranked by impact. Fix CRITICAL issues before touching MEDIUM ones.
适用于React 19 + Vite + Cloudflare Workers项目的性能与组合模式。可作为编写新组件时的检查清单、审计现有代码时的评审指南,或是在页面卡顿或代码混乱时的重构手册。
规则按影响优先级排序。先修复CRITICAL(严重)问题,再处理MEDIUM(中等)问题。

When to Apply

适用场景

  • Writing new React components or pages
  • Reviewing code for performance issues
  • Refactoring components with too many props or re-renders
  • Debugging "why is this slow?" or "why does this re-render?"
  • Building reusable component libraries
  • Code review before merging
  • 编写新的React组件或页面
  • 评审代码中的性能问题
  • 重构拥有过多属性或重复渲染的组件
  • 调试“为什么这么卡?”或“为什么会重复渲染?”的问题
  • 构建可复用组件库
  • 代码合并前的评审

1. Eliminating Waterfalls (CRITICAL)

1. 消除请求瀑布(CRITICAL)

Sequential async calls where they could be parallel. The #1 performance killer.
PatternProblemFix
Await in sequence
const a = await getA(); const b = await getB();
const [a, b] = await Promise.all([getA(), getB()]);
Fetch in childParent renders, then child fetches, then grandchild fetchesHoist fetches to the highest common ancestor, pass data down
Suspense cascadeMultiple Suspense boundaries that resolve sequentiallyOne Suspense boundary wrapping all async siblings
Await before branch
const data = await fetch(); if (condition) { use(data); }
Move await inside the branch — don't fetch what you might not use
Import then render
const Component = await import('./Heavy'); return <Component />
Use
React.lazy()
+
<Suspense>
— renders fallback instantly
How to find them: Search for
await
in components. Each
await
is a potential waterfall. If two awaits are independent, they should be parallel.
本应并行的异步调用却按顺序执行,这是头号性能杀手。
模式问题修复方案
顺序await
const a = await getA(); const b = await getB();
const [a, b] = await Promise.all([getA(), getB()]);
子组件中发起请求父组件渲染,然后子组件发起请求,接着孙组件发起请求将请求提升到最近的共同祖先组件,再向下传递数据
Suspense级联多个Suspense边界按顺序解析用一个Suspense边界包裹所有异步兄弟组件
分支判断前await
const data = await fetch(); if (condition) { use(data); }
将await移到分支内部——不要请求可能用不到的数据
先导入再渲染
const Component = await import('./Heavy'); return <Component />
使用
React.lazy()
+
<Suspense>
——立即渲染占位内容
排查方法:在组件中搜索
await
关键字。每个
await
都可能是请求瀑布的源头。如果两个await是独立的,就应该并行执行。

2. Bundle Size (CRITICAL)

2. 打包体积优化(CRITICAL)

Every KB the user downloads is a KB they wait for.
PatternProblemFix
Barrel imports
import { Button } from '@/components'
pulls the entire barrel file
import { Button } from '@/components/ui/button'
— direct import
No code splittingHeavy component loaded on every page
React.lazy(() => import('./HeavyComponent'))
+
<Suspense>
Third-party at loadAnalytics/tracking loaded before the app rendersLoad after hydration:
useEffect(() => { import('./analytics') }, [])
Full library import
import _ from 'lodash'
(70KB)
import debounce from 'lodash/debounce'
(1KB)
Lucide tree-shaking
import * as Icons from 'lucide-react'
(all icons)
Explicit map:
import { Home, Settings } from 'lucide-react'
Duplicate ReactLibrary bundles its own React → "Cannot read properties of null"
resolve.dedupe: ['react', 'react-dom']
in vite.config.ts
How to find them:
npx vite-bundle-visualizer
— shows what's in your bundle.
用户下载的每1KB内容都意味着等待时间的增加。
模式问题修复方案
桶导入
import { Button } from '@/components'
会引入整个桶文件
直接导入:
import { Button } from '@/components/ui/button'
未做代码分割大型组件在所有页面都被加载使用
React.lazy(() => import('./HeavyComponent'))
+
<Suspense>
第三方库初始加载分析/追踪库在应用渲染前就被加载hydration后加载:
useEffect(() => { import('./analytics') }, [])
导入完整库
import _ from 'lodash'
(70KB)
按需导入:
import debounce from 'lodash/debounce'
(1KB)
Lucide图标未摇树
import * as Icons from 'lucide-react'
(所有图标)
显式导入:
import { Home, Settings } from 'lucide-react'
重复React实例库自带了React → 出现"Cannot read properties of null"错误在vite.config.ts中配置
resolve.dedupe: ['react', 'react-dom']
排查方法:执行
npx vite-bundle-visualizer
——查看打包内容的构成。

3. Composition Architecture (HIGH)

3. 组合架构(HIGH)

How you structure components matters more than how you optimise them.
PatternProblemFix
Boolean prop explosion
<Card isCompact isClickable showBorder hasIcon isLoading>
Explicit variants:
<CompactCard>
,
<ClickableCard>
Compound componentsComplex component with 15 propsSplit into
<Dialog>
,
<Dialog.Trigger>
,
<Dialog.Content>
with shared context
renderX props
<Layout renderSidebar={...} renderHeader={...} renderFooter={...}>
Use children + named slots:
<Layout><Sidebar /><Header /></Layout>
Lift stateSibling components can't share stateMove state to parent or context provider
Provider implementationConsumer code knows about state management internalsProvider exposes interface
{ state, actions, meta }
— implementation hidden
Inline components
function Parent() { function Child() { ... } return <Child /> }
Define Child outside Parent — inline components remount on every render
The test: If a component has more than 5 boolean props, it needs composition, not more props.
组件的结构设计比优化技巧更重要。
模式问题修复方案
布尔属性爆炸
<Card isCompact isClickable showBorder hasIcon isLoading>
显式变体:
<CompactCard>
,
<ClickableCard>
复合组件拥有15个属性的复杂组件拆分为
<Dialog>
,
<Dialog.Trigger>
,
<Dialog.Content>
,通过共享上下文通信
renderX属性
<Layout renderSidebar={...} renderHeader={...} renderFooter={...}>
使用子组件+命名插槽:
<Layout><Sidebar /><Header /></Layout>
状态提升兄弟组件无法共享状态将状态移到父组件或上下文提供者中
提供者实现消费者代码知晓状态管理的内部细节提供者暴露接口
{ state, actions, meta }
——隐藏实现细节
内联组件
function Parent() { function Child() { ... } return <Child /> }
将Child定义在Parent外部——内联组件会在每次渲染时重新挂载
测试标准:如果一个组件的布尔属性超过5个,就需要通过组合重构,而不是增加更多属性。

4. Re-render Prevention (MEDIUM)

4. 防止重复渲染(MEDIUM)

Not all re-renders are bad. Only fix re-renders that cause visible jank or wasted computation.
PatternProblemFix
Default object/array props
function Foo({ items = [] })
→ new array ref every render
Hoist:
const DEFAULT = []; function Foo({ items = DEFAULT })
Derived state in effect
useEffect(() => setFiltered(items.filter(...)), [items])
Derive during render:
const filtered = useMemo(() => items.filter(...), [items])
Object dependency
useEffect(() => {...}, [config])
fires every render if config is
{}
Use primitive deps:
useEffect(() => {...}, [config.id, config.type])
Subscribe to unused stateComponent reads
{ user, theme, settings }
but only uses
user
Split context or use selector:
useSyncExternalStore
State for transient values
const [mouseX, setMouseX] = useState(0)
on mousemove
Use
useRef
for values that change frequently but don't need re-render
Inline callback props
<Button onClick={() => doThing(id)} />
— new function every render
useCallback
or functional setState:
<Button onClick={handleClick} />
How to find them: React DevTools Profiler → "Why did this render?" or
<React.StrictMode>
double-renders in dev.
并非所有重复渲染都是问题。仅修复那些导致可见卡顿或计算浪费的重复渲染。
模式问题修复方案
默认对象/数组属性
function Foo({ items = [] })
→ 每次渲染都会创建新数组引用
提升默认值:
const DEFAULT = []; function Foo({ items = DEFAULT })
Effect中派生状态
useEffect(() => setFiltered(items.filter(...)), [items])
渲染时派生:
const filtered = useMemo(() => items.filter(...), [items])
对象依赖项
useEffect(() => {...}, [config])
如果config是
{}
,每次渲染都会触发
使用原始类型依赖:
useEffect(() => {...}, [config.id, config.type])
订阅未使用的状态组件读取
{ user, theme, settings }
但只使用
user
拆分上下文或使用选择器:
useSyncExternalStore
瞬态值用状态存储
const [mouseX, setMouseX] = useState(0)
监听mousemove
使用
useRef
存储频繁变化但不需要触发渲染的值
内联回调属性
<Button onClick={() => doThing(id)} />
——每次渲染都会创建新函数
使用
useCallback
或函数式setState:
<Button onClick={handleClick} />
排查方法:React DevTools Profiler → "Why did this render?" 或在开发环境中使用
<React.StrictMode>
的双重渲染检测。

5. React 19 Specifics (MEDIUM)

5. React 19专属特性(MEDIUM)

Patterns that changed or are new in React 19.
PatternOld (React 18)New (React 19)
Form state
useFormState
useActionState
— renamed
Ref forwarding
forwardRef((props, ref) => ...)
function Component({ ref, ...props })
— ref is a regular prop
Context
useContext(MyContext)
use(MyContext)
— works in conditionals and loops
Pending UIManual loading state
useTransition
+
startTransition
for non-urgent updates
Route-level lazyWorks with
createBrowserRouter
only
Still true —
<Route lazy={...}>
is silently ignored with
<BrowserRouter>
Optimistic updatesManual state management
useOptimistic
hook
MetadataHelmet or manual
<head>
management
<title>
,
<meta>
,
<link>
in component JSX — hoisted to
<head>
automatically
React 19中变更或新增的模式。
模式旧版(React 18)新版(React 19)
表单状态
useFormState
useActionState
——重命名
Ref转发
forwardRef((props, ref) => ...)
function Component({ ref, ...props })
——ref是常规属性
上下文
useContext(MyContext)
use(MyContext)
——可在条件判断和循环中使用
等待状态UI手动管理加载状态
useTransition
+
startTransition
处理非紧急更新
路由级懒加载仅支持
createBrowserRouter
依然如此——使用
<BrowserRouter>
<Route lazy={...}>
会被静默忽略
乐观更新手动管理状态
useOptimistic
Hook
元数据Helmet或手动管理
<head>
在组件JSX中直接使用
<title>
<meta>
<link>
——会自动提升到
<head>

6. Rendering Performance (MEDIUM)

6. 渲染性能优化(MEDIUM)

PatternProblemFix
Layout shift on loadContent jumps when async data arrivesSkeleton screens matching final layout dimensions
Animate SVG directlyJanky SVG animationWrap in
<div>
, animate the div instead
Large list rendering1000+ items in a table/list
@tanstack/react-virtual
for virtualised rendering
content-visibilityLong scrollable content renders everything upfront
content-visibility: auto
on off-screen sections
Conditional render with &&
{count && <Items />}
renders
0
when count is 0
Use ternary:
{count > 0 ? <Items /> : null}
模式问题修复方案
加载时布局偏移异步数据到达时内容跳动使用与最终布局尺寸匹配的骨架屏
直接动画SVGSVG动画卡顿包裹在
<div>
中,为div添加动画
大型列表渲染表格/列表中有1000+条数据使用
@tanstack/react-virtual
实现虚拟滚动
content-visibility长滚动内容初始时渲染所有元素为屏幕外的部分添加
content-visibility: auto
样式
用&&做条件渲染
{count && <Items />}
当count为0时会渲染
0
使用三元表达式:
{count > 0 ? <Items /> : null}

7. Data Fetching (MEDIUM)

7. 数据请求优化(MEDIUM)

PatternProblemFix
No deduplicationSame data fetched by 3 componentsTanStack Query or SWR — automatic dedup + caching
Fetch on mount
useEffect(() => { fetch(...) }, [])
— waterfalls, no caching, no dedup
TanStack Query:
useQuery({ queryKey: ['users'], queryFn: fetchUsers })
No optimistic updateUser clicks save, waits 2 seconds, then sees change
useMutation
with
onMutate
for instant visual feedback
Stale closure in interval
setInterval
captures stale state
useRef
for the interval ID and current values
Polling without cleanup
setInterval
in useEffect without
clearInterval
Return cleanup:
useEffect(() => { const id = setInterval(...); return () => clearInterval(id); })
模式问题修复方案
无请求去重3个组件请求相同数据使用TanStack Query或SWR——自动去重+缓存
挂载时请求
useEffect(() => { fetch(...) }, [])
——存在请求瀑布、无缓存、无去重
TanStack Query:
useQuery({ queryKey: ['users'], queryFn: fetchUsers })
无乐观更新用户点击保存后,等待2秒才看到变化使用
useMutation
配合
onMutate
实现即时视觉反馈
定时器中的闭包过期
setInterval
捕获过期状态
使用
useRef
存储定时器ID和当前值
无清理的轮询useEffect中的
setInterval
未调用
clearInterval
返回清理函数:
useEffect(() => { const id = setInterval(...); return () => clearInterval(id); })

8. Vite + Cloudflare Specifics (MEDIUM)

8. Vite + Cloudflare专属配置(MEDIUM)

PatternProblemFix
import.meta.env
in Node scripts
Undefined — only works in Vite-processed filesUse
loadEnv()
from vite
React duplicate instanceLibrary bundles its own React
resolve.dedupe
+
optimizeDeps.include
in vite.config.ts
Radix Select empty string
<SelectItem value="">
throws
Use sentinel:
<SelectItem value="__any__">
React Hook Form null
{...field}
passes null to Input
Spread manually:
value={field.value ?? ''}
Env vars at edge
process.env
doesn't exist in Workers
Use
c.env
(Hono context) or
import.meta.env
(Vite build-time)
模式问题修复方案
Node脚本中使用
import.meta.env
未定义——仅在Vite处理的文件中可用使用Vite的
loadEnv()
重复React实例库自带了React在vite.config.ts中配置
resolve.dedupe
+
optimizeDeps.include
Radix Select空字符串值
<SelectItem value="">
抛出错误
使用标记值:
<SelectItem value="__any__">
React Hook Form空值问题
{...field}
向Input传递null
手动展开:
value={field.value ?? ''}
边缘环境中的环境变量Workers中不存在
process.env
使用
c.env
(Hono上下文)或
import.meta.env
(Vite构建时)

Using as a Review Checklist

作为代码评审检查清单使用

When reviewing code, go through categories 1-3 (CRITICAL + HIGH) for every PR. Categories 4-8 only when performance is a concern.
/react-patterns [file or component path]
Read the file, check against rules in priority order, report findings as:
file:line — [rule] description of issue
评审代码时,对每个PR都要检查第1-3类(CRITICAL + HIGH)的规则。仅当存在性能问题时才需要检查第4-8类。
/react-patterns [文件或组件路径]
阅读文件,按优先级对照规则检查,按以下格式报告问题:
file:line — [规则] 问题描述
```",