ia-react-frontend

提供 React、TypeScript、Next.js 等技术的架构与编码规范,涵盖状态管理、性能优化与测试实践。

已扫描
适合谁
前端开发工程师、技术负责人或架构师
不适合谁
初学者快速上手项目、非前端开发人员
国内可用性
需网络配置。可能需要网络配置或第三方服务可访问。
安装难度
新手友好(★☆☆)。基于终端操作、依赖、API Key 和本地环境要求的初步判断。

安装与下载

openclaw skills install @iliaal/compound-eng-react-frontend

Skill 说明

命令、参数、文件名以原文为准

React 前端

实施前验证:对于 App Router 模式、React 19 API 或版本特定行为,请通过 Context7 (query-docs) 查阅最新文档,再编写代码。训练数据可能滞后于最新发布版本。

组件 TypeScript

  • 使用 ComponentPropsWithoutRef<'button'> 扩展原生元素,通过交叉类型添加自定义属性
  • 子节点使用 React.ReactNode,单个元素使用 React.ReactElement,渲染函数使用 (data: T) => ReactNode
  • 变体属性使用区分联合类型 —— TypeScript 在分支中会自动缩小类型
  • 泛型组件:<T> 配合 keyof T 表示列键,T extends { id: string } 添加约束
  • 事件类型:React.MouseEvent<HTMLButtonElement>FormEvent<HTMLFormElement>ChangeEvent<HTMLInputElement>
  • 使用 as const 标记自定义 Hook 的元组返回值
  • DOM 引用使用 useRef<HTMLInputElement>(null)(配合 ?.),可变值使用 useRef<number>(0)
  • 显式声明 useState<User | null>(null) 处理联合类型或空值
  • useReducer 动作使用区分联合类型:{ type: 'set'; payload: number } | { type: 'reset' }
  • useContext 空值保护:在自定义 useX() Hook 中,若上下文为 null 则抛出异常

Effects 决策树

Effects 是逃生通道 —— 大多数逻辑不应使用 Effects。

需求解决方案
从 props 或 state 派生的值在渲染期间计算(如开销大,使用 useMemo)
prop 变化时重置状态使用组件的 key 属性
响应用户事件事件处理器
通知父组件状态变化在事件处理器中调用 onChange,或使用完全受控组件
状态更新链在一个事件处理器中一次性计算所有下一状态
与外部系统同步使用带清理函数的 Effect

Effect 规则:

  • 永远不要禁用 linter —— 应修复代码
  • 使用更新函数(setItems(prev => [...prev, item]))以消除状态依赖
  • 将对象/函数移入 Effects 内部,以稳定依赖项
  • 对非响应式值使用 useEffectEvent(例如连接中的主题)
  • 始终返回清理函数,用于订阅、连接、监听器
  • 数据获取取消处理(根据场景选择):AbortController 用于 fetch;ignore 标志用于不可取消的 Promise;React Query 自动处理两者

并发与竞态类别

前端 bug 在通过类型检查和单元测试后,通常属于以下五类竞态。审查时需明确排查:

  1. 生命周期清理遗漏 —— 生产环境信号:"无法在已卸载组件上执行状态更新" 警告,快速导航下的缓慢内存泄漏,重复事件处理器触发。根本原因:useEffect 注册了监听器/定时器/观察者但未返回清理函数(参见 Effect 规则)。
  2. 重新挂载时机错误 —— 异步回调在组件交换 / 断开 / 路由变更后修改 DOM 或状态。典型情况:fetch().then(setData) 在导航到不同路由后才完成;requestAnimationFrame 在父组件卸载后仍触发。参见 Effect 规则中的“数据获取”部分关于取消层级。
  3. UI 状态使用布尔值表示非二元状态 —— isLoading: boolean 无法表达 idle | loading | success | error | retry,否则会产生不一致组合(如 isLoading: true, error: Error 相互矛盾)。建议使用显式状态常量('idle' | 'loading' | 'success' | 'error')并配合转换函数,使无效状态不可达。
  4. 无取消路径的过期 Promise 和定时器 —— 一个 Promise 链或 setTimeout 保留对 setState 的引用,而组件已离开。每个异步操作都必须绑定到取消机制(参见上述取消层级),并通过测试验证清理路径被正确执行。
  5. 每项元素都绑定处理器,而应使用事件委托 —— 为列表中每一行绑定 onClick 会创建 N 个闭包和 N 个订阅;使用单一父级处理器,通过 event.target.closest(...) 读取目标,更安全,避免在频繁重渲染下出现闭包过期问题,并支持大规模列表。当列表超过约 50 项或频繁更新时,应优先使用事件委托。

这些类别产生的 bug 具有间歇性、环境依赖性和类型检查无法发现的特点 —— 正是那些进入生产环境的问题。必须主动审查,而非仅关注“订阅需要清理”。

状态管理

本地 UI 状态       → useState, useReducer
共享客户端状态    → Zustand (简单) | Redux Toolkit (复杂)
原子化/细粒度      → Jotai
服务器/远程数据    → React Query (TanStack Query)
URL 状态           → nuqs, router search params
表单状态           → React Hook Form

关键模式:

  • Zustand:create<State>()(devtools(persist((set) => ({...})))) —— 使用切片提升可扩展性,选择性订阅防止不必要的重渲染
  • React Query:查询键工厂(['users', 'detail', id] as const),staleTime/gcTime,通过 onMutate/onError 实现乐观更新与回滚
  • 分离客户端状态(Zustand)与服务器状态(React Query)—— 永不将服务器数据重复存储在客户端状态中
  • 将状态就近放置于使用位置;避免过度全局化

性能

关键 —— 消除瀑布流:

  • 对独立异步操作使用 Promise.all()
  • await 移至实际需要的位置
  • 使用 Suspense 边界实现慢速内容的流式加载

关键 —— 包体积优化:

  • 直接从模块导入,避免 barrel 文件(index.ts 重新导出)
  • 对重型组件使用 next/dynamicReact.lazy()
  • 延迟第三方脚本(分析、日志)直到 hydration 完成后
  • 在悬停/聚焦时预加载以提升感知速度
  • 长列表使用 content-visibility: auto + contain-intrinsic-size —— 跳过屏幕外的布局/绘制

重新渲染优化:

  • 在渲染期间推导状态,而非在副作用中
  • 订阅派生的布尔值,而非原始对象(state.items.length > 0 而非 state.items
  • 使用函数式 setState 以保持回调稳定:setCount(c => c + 1)
  • 延迟初始化状态:useState(() => expensiveComputation())
  • 对非紧急更新使用 useTransition(如搜索过滤)
  • 对昂贵的派生 UI 使用 useDeferredValue
  • 如果仅在回调中读取 searchParams/state,则不要订阅——改为按需读取
  • 使用三元表达式(condition ? <A /> : <B />),而非 && 实现条件渲染
  • React.memo 仅用于具有稳定 props 的昂贵子树
  • 将静态 JSX 提升到组件外部

React Compiler(React 19):自动记忆化——编写符合惯例的 React 代码,移除手动的 useMemo/useCallback/memo。通过框架配置启用(Next.js:在 next.config 中设置 reactCompiler: true)。非框架项目:安装 babel-plugin-react-compiler。保持组件纯净。

React 19

  • ref 作为 prop —— forwardRef 已弃用。将 ref?: React.Ref<HTMLElement> 作为普通 prop 接收
  • useActionState —— 替代 useFormStateconst [state, formAction, isPending] = useActionState(action, initialState)
  • use() —— 在渲染期间解包 Promise 或 Context(不在回调或副作用中)。支持条件性上下文读取
  • useOptimistic —— const [optimistic, addOptimistic] = useOptimistic(state, mergeFn),用于即时 UI 反馈
  • useFormStatus —— 在 <form action={...}> 的子组件中使用:const { pending } = useFormStatus()
  • 服务器组件 —— App Router 默认启用。支持异步操作,可直接访问数据库或密钥。不支持 hooks,不支持事件处理器
  • 服务器动作 —— 使用 'use server' 指令。验证输入(如 Zod),在变更后调用 revalidateTag/revalidatePath服务器动作是公开端点——每个动作内部都必须验证身份认证和权限,不能仅依赖中间件或布局守卫
  • **<Activity mode='visible'|'hidden'>** —— 保留切换组件的状态和 DOM(实验性)

Next.js App Router

文件命名规范:

page.tsx(路由界面)

layout.tsx(共享包装器)

template.tsx(导航时重新挂载,不同于 layout)

loading.tsx(Suspense)

error.tsx(错误边界)

not-found.tsx(404 页面)

default.tsx(并行路由的回退)

route.ts(API 端点)

渲染模式:

服务器组件(默认) | 客户端('use client') | 静态(构建时) | 动态(请求时) | 流式(渐进式)

决策原则:

除非需要 hooks、事件处理器或浏览器 API,否则使用服务器组件。拆分结构:服务器父组件 + 客户端子组件。将交互式组件作为 'use client' 的叶子组件隔离——保持服务器组件静态,无全局状态或事件处理器。

路由模式:

  • 路由组 (name) —— 用于组织,不影响 URL
  • 并行路由 @slot —— 同一布局中独立的加载状态
  • 截获路由 (.) —— 模态覆盖层,带完整页面回退

缓存策略:

  • fetch(url, { cache: 'force-cache' }) —— 静态缓存
  • fetch(url, { next: { revalidate: 60 } }) —— ISR(增量静态再生)
  • fetch(url, { cache: 'no-store' }) —— 动态请求
  • 标签机制:fetch(url, { next: { tags: ['products'] } }),随后调用 revalidateTag('products')

数据获取:

在使用数据的服务器组件中进行 fetch。对慢查询使用 Suspense 边界。使用 React.cache() 实现每请求去重。使用 generateStaticParams 进行静态生成。使用 generateMetadata 实现动态 SEO。静态元数据使用 title: { default: 'App', template: '%s | App' } 实现页面标题级联。使用 after() 执行非阻塞副作用(如日志、分析)——在响应发送后运行。将静态 I/O(字体、配置)提升至模块级别——仅执行一次,而非每次请求都执行。

测试(Vitest + React Testing Library)

  • 组件测试:Vitest + RTL,与组件同目录的 *.test.tsx 文件。适用于 React 组件的默认测试方式
  • 钩子测试:使用 renderHook + act,同目录的 *.test.ts 文件
  • 单元测试:Vitest 用于纯函数、工具函数、服务
  • 端到端测试:Playwright 用于用户流程和关键路径
  • 查询优先级getByRole > getByLabelText > getByPlaceholderText > getByText > getByTestId
  • 模拟 API 服务和外部提供者;真实渲染子组件以保证集成信心
  • 每个测试只验证一个行为,采用 AAA 结构(Arrange, Act, Assert)。命名格式:should <行为> when <条件>
  • 使用 userEvent 而非 fireEvent,以模拟真实交互
  • 对异步元素使用 findBy*,在状态触发动作后使用 waitFor
  • beforeEach 中调用 vi.clearAllMocks()。每次测试重建状态
  • 通用测试纪律(反模式、理性抵抗):参见 [ia-writing-tests](../writing-tests/SKILL.md) 技能文档

参考 [testing patterns and examples](./references/testing.md) 获取组件、钩子和模拟示例

参考 [e2e testing](./references/e2e-testing.md) 获取 Playwright 测试模式

Tailwind 集成

关于 Tailwind v4 的配置、实用类模式、暗色模式、组件变体等内容,请参见 [ia-tailwind-css](../tailwind-css/SKILL.md) 技能文档。

JSX 中的类名排序:当使用 clsxcvacntvtw 等工具函数时,保持 Tailwind 类名按标准顺序排列。通过配置 eslint-plugin-better-tailwindcss 插件,启用 useSortedClasses 并设置 functions: ["clsx", "cva", "cn", "tv", "tw"],可自动强制在 JSX 属性和辅助函数调用中保持类名有序。

规范

  • 优先考虑简洁性——每次变更尽可能简单,影响最小范围的代码
  • 仅修改必要部分——避免引入无关更改
  • 不使用临时 workaround——若修复感觉不妥,应退回并实现更清晰的方案
  • 在新增抽象前,确认其已在 3 处以上出现

参考

  • [testing.md](./references/testing.md) —— 组件、钩子及模拟测试示例
  • [e2e-testing.md](./references/e2e-testing.md) —— Playwright 端到端测试模式

验证

  • TypeScript 编译无错误
  • 新代码中不使用被禁用的 lint 规则(如 eslint-disable@ts-ignore
  • useEffect 依赖项数组不手动覆盖
  • React 19+ 项目中不使用 forwardRef(直接使用 ref 属性)
I
@iliaal

已收录 1 个 Skill

相关推荐