react-patterns by jezweb/claude-skills
npx skills add https://github.com/jezweb/claude-skills --skill react-patterns适用于 React 19 + Vite + Cloudflare Workers 项目的性能和组合模式。在编写新组件时用作检查清单,在审计现有代码时用作审查指南,或在感觉某些部分运行缓慢或逻辑混乱时用作重构手册。
规则按影响程度排序。在处理中等优先级问题之前,先修复关键问题。
本可以并行执行的异步调用却顺序执行。这是头号性能杀手。
| 模式 | 问题 | 修复方案 |
|---|---|---|
| 顺序等待 | const a = await getA(); const b = await getB(); | const [a, b] = await Promise.all([getA(), getB()]); |
| 在子组件中获取数据 | 父组件渲染,然后子组件获取数据,然后孙组件获取数据 | 将数据获取提升到最近的共同祖先组件,向下传递数据 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 多个按顺序解析的 Suspense 边界 |
| 使用一个 Suspense 边界包裹所有异步兄弟组件 |
| 分支前等待 | const data = await fetch(); if (condition) { use(data); } | 将 await 移到分支内部 —— 不要获取可能用不到的数据 |
| 导入后渲染 | const Component = await import('./Heavy'); return <Component /> | 使用 React.lazy() + <Suspense> —— 立即渲染回退内容 |
如何发现它们:在组件中搜索 await。每个 await 都是一个潜在的瀑布。如果两个 await 是独立的,它们应该并行执行。
用户下载的每一个 KB 都是他们需要等待的。
| 模式 | 问题 | 修复方案 |
|---|---|---|
| 桶导入 | import { Button } from '@/components' 会拉取整个桶文件 | import { Button } from '@/components/ui/button' —— 直接导入 |
| 没有代码分割 | 在每个页面都加载重型组件 | React.lazy(() => import('./HeavyComponent')) + <Suspense> |
| 加载时引入第三方库 | 在应用渲染前加载分析/跟踪代码 | 在注水后加载: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 —— 显示你的打包内容。
如何组织组件比如何优化它们更重要。
| 模式 | 问题 | 修复方案 |
|---|---|---|
| 布尔属性爆炸 | <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 个布尔属性,它需要的是组合,而不是更多属性。
并非所有重新渲染都是坏的。只修复导致可见卡顿或浪费计算的重新渲染。
| 模式 | 问题 | 修复方案 |
|---|---|---|
| 默认对象/数组属性 | function Foo({ items = [] }) → 每次渲染都创建新的数组引用 | 提升:const DEFAULT = []; function Foo({ items = DEFAULT }) |
| 在副作用中派生状态 | 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> 的双重渲染。
在 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 钩子 |
| 元数据 | Helmet 或手动管理 <head> | 在组件 JSX 中使用 <title>、<meta>、<link> —— 自动提升到 <head> |
| 模式 | 问题 | 修复方案 |
|---|---|---|
| 加载时的布局偏移 | 异步数据到达时内容跳动 | 使用与最终布局尺寸匹配的骨架屏 |
| 直接为 SVG 添加动画 | SVG 动画卡顿 | 用 <div> 包裹,改为为 div 添加动画 |
| 大型列表渲染 | 表格/列表中有 1000+ 项 | 使用 @tanstack/react-virtual 进行虚拟化渲染 |
| content-visibility | 长滚动内容一次性渲染所有内容 | 对屏幕外部分使用 content-visibility: auto |
| 使用 && 的条件渲染 | {count && <Items />} 在 count 为 0 时会渲染 0 | 使用三元运算符:{count > 0 ? <Items /> : null} |
| 模式 | 问题 | 修复方案 |
|---|---|---|
| 没有去重 | 相同的数据被 3 个组件获取 | 使用 TanStack Query 或 SWR —— 自动去重 + 缓存 |
| 在挂载时获取 | useEffect(() => { fetch(...) }, []) —— 瀑布式加载,无缓存,无去重 | TanStack Query:useQuery({ queryKey: ['users'], queryFn: fetchUsers }) |
| 没有乐观更新 | 用户点击保存,等待 2 秒,然后看到变化 | 使用带有 onMutate 的 useMutation 实现即时视觉反馈 |
| 间隔函数中的陈旧闭包 | setInterval 捕获了过时的状态 | 使用 useRef 存储间隔 ID 和当前值 |
| 轮询没有清理 | 在 useEffect 中使用 setInterval 但没有 clearInterval | 返回清理函数:useEffect(() => { const id = setInterval(...); return () => clearInterval(id); }) |
| 模式 | 问题 | 修复方案 |
|---|---|---|
在 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 null 值 | {...field} 将 null 传递给 Input | 手动展开:value={field.value ?? ''} |
| 边缘环境变量 | process.env 在 Workers 中不存在 | 使用 c.env(Hono 上下文)或 import.meta.env(Vite 构建时) |
审查代码时,为每个 PR 检查类别 1-3(关键 + 高)。仅当关注性能时才检查类别 4-8。
/react-patterns [文件或组件路径]
读取文件,按优先级顺序对照规则检查,报告发现的问题:
文件:行号 — [规则] 问题描述
每周安装次数
123
仓库
GitHub 星标数
650
首次出现
7 天前
安全审计
安装于
opencode117
github-copilot117
cursor117
gemini-cli116
kimi-cli116
codex116
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.
Sequential async calls where they could be parallel. The #1 performance killer.
| Pattern | Problem | Fix |
|---|---|---|
| Await in sequence | const a = await getA(); const b = await getB(); | const [a, b] = await Promise.all([getA(), getB()]); |
| Fetch in child | Parent renders, then child fetches, then grandchild fetches | Hoist fetches to the highest common ancestor, pass data down |
| Suspense cascade | Multiple Suspense boundaries that resolve sequentially | One 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.
Every KB the user downloads is a KB they wait for.
| Pattern | Problem | Fix |
|---|---|---|
| Barrel imports | import { Button } from '@/components' pulls the entire barrel file | import { Button } from '@/components/ui/button' — direct import |
| No code splitting | Heavy component loaded on every page | React.lazy(() => import('./HeavyComponent')) + <Suspense> |
| Third-party at load | Analytics/tracking loaded before the app renders | Load after hydration: useEffect(() => { import('./analytics') }, []) |
How to find them : npx vite-bundle-visualizer — shows what's in your bundle.
How you structure components matters more than how you optimise them.
| Pattern | Problem | Fix |
|---|---|---|
| Boolean prop explosion | <Card isCompact isClickable showBorder hasIcon isLoading> | Explicit variants: <CompactCard>, <ClickableCard> |
| Compound components | Complex component with 15 props | Split into <Dialog>, <Dialog.Trigger>, <Dialog.Content> with shared context |
| renderX props |
The test : If a component has more than 5 boolean props, it needs composition, not more props.
Not all re-renders are bad. Only fix re-renders that cause visible jank or wasted computation.
| Pattern | Problem | Fix |
|---|---|---|
| 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 |
How to find them : React DevTools Profiler → "Why did this render?" or <React.StrictMode> double-renders in dev.
Patterns that changed or are new in React 19.
| Pattern | Old (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 |
| Pattern | Problem | Fix |
|---|---|---|
| Layout shift on load | Content jumps when async data arrives | Skeleton screens matching final layout dimensions |
| Animate SVG directly | Janky SVG animation | Wrap in <div>, animate the div instead |
| Large list rendering | 1000+ items in a table/list | @tanstack/react-virtual for virtualised rendering |
| content-visibility | Long scrollable content renders everything upfront | content-visibility: auto on off-screen sections |
| Conditional render with && | renders when count is 0 |
| Pattern | Problem | Fix |
|---|---|---|
| No deduplication | Same data fetched by 3 components | TanStack 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 update | User clicks save, waits 2 seconds, then sees change | useMutation with onMutate for instant visual feedback |
| Stale closure in interval | captures stale state |
| Pattern | Problem | Fix |
|---|---|---|
import.meta.env in Node scripts | Undefined — only works in Vite-processed files | Use loadEnv() from vite |
| React duplicate instance | Library bundles its own React | resolve.dedupe + optimizeDeps.include in vite.config.ts |
| Radix Select empty string | <SelectItem value=""> throws | Use sentinel: <SelectItem value="__any__"> |
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
Weekly Installs
123
Repository
GitHub Stars
650
First Seen
7 days ago
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode117
github-copilot117
cursor117
gemini-cli116
kimi-cli116
codex116
GSAP 框架集成指南:Vue、Svelte 等框架中 GSAP 动画最佳实践
2,800 周安装
| 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 React | Library bundles its own React → "Cannot read properties of null" | resolve.dedupe: ['react', 'react-dom'] in vite.config.ts |
<Layout renderSidebar={...} renderHeader={...} renderFooter={...}> |
Use children + named slots: <Layout><Sidebar /><Header /></Layout> |
| Lift state | Sibling components can't share state | Move state to parent or context provider |
| Provider implementation | Consumer code knows about state management internals | Provider 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 |
{}Use primitive deps: useEffect(() => {...}, [config.id, config.type]) |
| Subscribe to unused state | Component 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} /> |
| Pending UI | Manual loading state | useTransition + startTransition for non-urgent updates |
| Route-level lazy | Works with createBrowserRouter only | Still true — <Route lazy={...}> is silently ignored with <BrowserRouter> |
| Optimistic updates | Manual state management | useOptimistic hook |
| Metadata | Helmet or manual <head> management | <title>, <meta>, <link> in component JSX — hoisted to <head> automatically |
{count && <Items />}0Use ternary: {count > 0 ? <Items /> : null} |
setIntervaluseRef for the interval ID and current values |
| Polling without cleanup | setInterval in useEffect without clearInterval | Return cleanup: useEffect(() => { const id = setInterval(...); return () => clearInterval(id); }) |
| 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) |