关于 react-patterns
面向 React 19、Vite 和 Cloudflare Workers 项目的 React 开发模式指南,可用于编写组件、代码评审和重构。内容按优先级覆盖性能、组件组合、数据获取及平台相关注意事项。
主要功能
- 01按优先级整理性能规则
- 02减少异步请求瀑布
- 03控制打包体积与组件重渲染
- 04组件组合与架构建议
- 05React 19 专属模式
- 06Vite 与 Cloudflare 注意事项
适用场景
- 01编写或评审 React 组件
- 02排查重复渲染或性能变慢
- 03重构属性过多的组件
- 04合并前检查代码变更
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill react-patterns安装后在 Claude Code 中输入/react-patterns确认已加载。
详细介绍
React 模式
适用于 React 19 + Vite + Cloudflare Workers 项目的性能与组合模式。编写新组件时可将其作为检查清单,审查现有代码时可将其作为指南,遇到性能缓慢或结构混乱时可将其作为重构手册。
规则按影响程度排序。先修复 CRITICAL 问题,再处理 MEDIUM 问题。
适用场景
- 编写新的 React 组件或页面
- 审查代码中的性能问题
- 重构属性过多或重复渲染的组件
- 调试“为什么这么慢?”或“为什么会重新渲染?”
- 构建可复用的组件库
- 合并前进行代码审查
1. 消除瀑布流(CRITICAL)
本可并行却按顺序执行的异步调用。这是头号性能杀手。
| 模式 | 问题 | 修复方法 |
|---|---|---|
| 按顺序等待 | const a = await getA(); const b = await getB(); | const [a, b] = await Promise.all([getA(), getB()]); |
| 在子组件中获取数据 | 父组件渲染后,子组件获取数据,然后孙组件再获取数据 | 将数据获取提升到最近的共同祖先组件,并向下传递数据 |
| Suspense 级联 | 多个 Suspense 边界依次完成 | 用一个 Suspense 边界包裹所有异步同级组件 |
| 分支判断前等待 | const data = await fetch(); if (condition) { use(data); } | 将 await 移入分支中——不要获取可能用不到的数据 |
| 导入后再渲染 | const Component = await import('./Heavy'); return <Component /> | 使用 React.lazy() + <Suspense>——立即渲染后备内容 |
如何查找:在组件中搜索 await。每个 await 都可能导致瀑布流。如果两个 await 彼此独立,就应该并行执行。
2. 包体积(CRITICAL)
用户下载的每 KB,都意味着他们要多等待一会儿。
| 模式 | 问题 | 修复方法 |
|---|---|---|
| 汇总导入 | 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. 组合架构(HIGH)
组件的组织方式比优化方式更重要。
| 模式 | 问题 | 修复方法 |
|---|---|---|
| 布尔属性泛滥 | <Card isCompact isClickable showBorder hasIcon isLoading> | 使用明确的变体:<CompactCard>、<ClickableCard> |
| 复合组件 | 包含 15 个属性的复杂组件 | 拆分为 <Dialog>、<Dialog.Trigger>、<Dialog.Content>,并共享上下文 |
| renderX 属性 | <Layout renderSidebar={...} renderHeader={...} renderFooter={...}> | 使用 children 和具名插槽:<Layout><Sidebar /><Header /></Layout> |
| 提升状态 | 同级组件无法共享状态 | 将状态移至父组件或上下文提供者 |
| 提供者实现 | 消费者代码了解状态管理的内部细节 | 提供者公开接口 { state, actions, meta }——隐藏具体实现 |
| 内联组件 | function Parent() { function Child() { ... } return <Child /> } | 在 Parent 外定义 Child——内联组件每次渲染都会重新挂载 |
检验标准:如果一个组件有 5 个以上的布尔属性,就应该采用组合方式,而不是继续增加属性。
4. 防止重复渲染(中)
并非所有重复渲染都是坏事。只修复会导致明显卡顿或浪费计算资源的重复渲染。
| 模式 | 问题 | 修复方法 |
|---|---|---|
| 默认对象/数组属性 | function Foo({ items = [] }) → 每次渲染都会创建新的数组引用 | 提升到外部:const DEFAULT = []; function Foo({ items = DEFAULT }) |
| 在 effect 中派生状态 | useEffect(() => setFiltered(items.filter(...)), [items]) | 在渲染期间派生:const filtered = useMemo(() => items.filter(...), [items]) |
| 对象依赖项 | 如果 config 是 {},useEffect(() => {...}, [config]) 会在每次渲染时触发 | 使用原始值依赖项:useEffect(() => {...}, [config.id, config.type]) |
| 订阅未使用的状态 | 组件读取 { user, theme, settings },但只使用 user | 拆分上下文或使用选择器:useSyncExternalStore |
| 将临时值存入状态 | 在 mousemove 事件中使用 const [mouseX, setMouseX] = useState(0) | 对频繁变化但不需要触发重复渲染的值使用 useRef |
| 内联回调属性 | <Button onClick={() => doThing(id)} /> — 每次渲染都会创建新函数 | 使用 useCallback 或函数式 setState:<Button onClick={handleClick} /> |
如何查找:使用 React DevTools Profiler 中的 “Why did this render?”,或在开发环境中使用 <React.StrictMode> 的双重渲染。
5. React 19 特性(中)
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. 渲染性能(中)
| 模式 | 问题 | 修复方法 |
|---|---|---|
| 加载时布局偏移 | 异步数据到达时,内容会跳动 | 使用尺寸与最终布局匹配的骨架屏 |
| 直接为 SVG 添加动画 | SVG 动画卡顿 | 用 <div> 包裹,并为该 div 添加动画 |
| 渲染大型列表 | 表格/列表中有 1000 项以上内容 | 使用 @tanstack/react-virtual 进行虚拟化渲染 |
| content-visibility | 可长距离滚动的内容会在页面加载时全部渲染 | 对屏幕外的区块设置 content-visibility: auto |
| 使用 && 条件渲染 | {count && <Items />} 在 count 为 0 时会渲染 0 | 使用三元表达式:{count > 0 ? <Items /> : null} |
7. 数据获取(中)
| 模式 | 问题 | 修复方法 |
|---|---|---|
| 未进行去重 | 3 个组件获取相同的数据 | 使用 TanStack Query 或 SWR — 自动去重并缓存 |
| 挂载时获取数据 | useEffect(() => { fetch(...) }, []) — 会产生瀑布请求,且没有缓存或去重 | 使用 TanStack Query:useQuery({ queryKey: ['users'], queryFn: fetchUsers }) |
| 没有乐观更新 | 用户点击保存后等待 2 秒,才看到更改 | 使用带有 onMutate 的 useMutation,即时提供视觉反馈 |
| interval 中的过时闭包 | setInterval 捕获了过时状态 | 使用 useRef 保存 interval ID 和当前值 |
| 轮询时未清理 | 在 useEffect 中使用 setInterval,但没有调用 clearInterval | 返回清理函数:useEffect(() => { const id = setInterval(...); return () => clearInterval(id); }) |
8. Vite + Cloudflare 特定问题(中)
| 模式 | 问题 | 修复方法 |
|---|---|---|
import.meta.env in Node scripts | 未定义——仅在经 Vite 处理的文件中有效 | 使用 vite 中的 loadEnv() |
| React 重复实例 | 库打包了自己的 React | 在 vite.config.ts 中使用 resolve.dedupe + optimizeDeps.include |
| Radix Select 空字符串 | <SelectItem value=""> 会抛出错误 | 使用哨兵值:<SelectItem value="__any__"> |
| React Hook Form null | {...field} 会将 null 传递给 Input | 手动展开:value={field.value ?? ''} |
| 边缘环境中的环境变量 | Workers 中不存在 process.env | 使用 c.env(Hono 上下文)或 import.meta.env(Vite 构建时) |
用作审查清单
审查代码时,每个 PR 都要检查类别 1-3(严重 + 高)。仅在关注性能时检查类别 4-8。
/react-patterns [file or component path]
阅读文件,按优先级顺序对照规则检查,并按以下格式报告发现:
file:line — [rule] description of issue
评论
支持 Markdown