React

构建、调试与审查 React 应用:组件、钩子、状态管理、服务端组件、表单、性能优化及测试。适用于编写或重构组件、选择状态管理方案(useState、Context、Zustand、TanStack Query、Redux)、解决组件过度重渲染或无限循环问题。

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

安装与下载

openclaw skills install @ivangdavila/react

Skill 说明

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

用户偏好和记忆数据存储在 ~/Clawic/data/react/ 目录中(首次使用时请参考 setup.md,文件格式可参见 memory-template.md)。若旧位置存在数据(如 ~/react/~/clawic/react/),请将其移至 ~/Clawic/data/react/

何时使用

  • 构建 React 组件、功能或应用架构
  • 选择或配置状态管理(useState、Context、Zustand、TanStack Query)
  • 使用 React 19:服务端组件、use()、动作(Actions)、React 编译器
  • 调试重渲染、无限循环、过期闭包、水合错误、数据获取竞争
  • 测试组件、为 props 添加类型、迁移旧版 React 代码
  • 审查 AI 生成的 React 代码是否包含此处列出的常见问题模式
  • 不适用于路由/部署相关细节(请使用 nextjs 技能)或移动端开发(请使用 react-native 技能)

快速参考

情况推荐方案
数据来自 API使用 TanStack Query — 避免直接使用 useState + useEffect 进行数据获取(参见 state.md
多个组件共享 UI 状态使用 Zustand 切片,并通过选择器读取(参见 state.md
单个组件使用的状态使用 useState,就近定义;仅当第二个消费者出现时才提升到上层
偶尔变化的全局状态(主题、语言、身份认证)使用 Context 并配合 memoized 值;将状态与分发逻辑拆分为两个 Context
用户可书签的筛选条件、标签页、页码使用 URL searchParams,而非 useState(参见 state.md
带验证的表单使用 React Hook Form + Zod(非受控:输入不触发重渲染);服务端提交使用 useActionState(参见 forms.md
过滤视图中输入延迟对接收的值使用 useDeferredValue;对设置的状态使用 startTransition
包含数千行的列表使用虚拟化(tanstack-virtual)—— DOM 节点数量是成本关键,而非数据量
组件需在身份变更时重置更改其 key 属性:<Profile key={userId} /> —— 重新挂载优于手动清除状态
多步骤流程 / 向导使用单一区分联合类型的字段,而非多个布尔值堆叠(参见 forms.md
程序崩溃导致整个应用空白为每个路由和高风险功能添加 react-error-boundary
相同组件反复提交但 props 未变先检查组件本身(参见 performance.md
无限循环、状态过期、水合错误参见 debug.md 中的症状链分析
代码库中存在类组件、forwardRef、PropTypes在新增功能前先阅读 migration.md
其他情况或不确定使用 colocated useState,命名导出;当第二个消费者出现时再提取

按需深入:debug.md 症状 → 原因链 · state.md 服务端/客户端/URL 状态 · server-components.md RSC 边界与动作 · hooks.md 效应规范、refs、自定义钩子 · performance.md 性能分析与修复优先级 · forms.md 验证、向导、文件上传 · testing.md Testing Library、MSW、异步测试 · typescript.md 类型模式 · accessibility.md 聚焦、ARIA、键盘支持 · migration.md 升级与遗留代码迁移 · setup.md 项目初始化与推荐技术栈

核心规则

  1. 服务端状态 ≠ 客户端状态。API 数据应由 TanStack Query 管理(一个具有生命周期的缓存);UI 状态使用 useState/Zustand。混合使用意味着需要手动重建去重、缓存和失效逻辑 —— 且极易出错。
  2. 无外部系统则无需副作用。若值可从 props 或 state 推导,则应在渲染中计算:const total = items.reduce(...)。编写 useEffect 前先问自己:“我正在同步哪个 React 外部系统?” 无答案 → 不需要副作用(参见 hooks.md 获取完整替代表)。
  3. **'use client' 标记的是边界,而非组件**。该文件导入的所有内容都会进入客户端包。应放置在交互性组件的最底层;将服务端组件作为 children 传递。
  4. 使用稳定键值 —— 键值也是工具。列表使用 key={item.id};主动更改键值是重置组件状态的官方方式。
  5. 仅使用命名导出export function UserCard —— 重命名重构更安全,导入也更易搜索。
  6. 按证据进行记忆化。启用 React 编译器时:不要手动写 memo/useMemo/useCallback —— 编译器会在违反 React 规则的组件上自动退出,不会修复它们。关闭编译器时:仅在性能分析器显示同一组件在 props 未变时仍重复提交后,才进行记忆化。
  7. **启用 strict: truenoUncheckedIndexedAccess**。开启后,array[0] 类型为 T | undefined —— 将原本可能运行时崩溃的情况提前变为编译错误。
  8. 在约 50 行 JSX 或每文件约 300 行代码处拆分(默认标准,非教条):超过此阈值时,可提取的组件已显而易见。

错误信息解析

错误原因首步操作
“过多重渲染”在渲染期间调用了 setState,且无条件执行查找函数体中的裸 setX(...),或 onClick={fn()} 而非 onClick={fn}
“本次渲染的钩子数量多于上次”条件性钩子,或钩子上方有提前返回将所有提前返回移至最后一个钩子之后(组件规则)
“无效的钩子调用”钩子不在组件或自定义钩子内 —— 或存在两个 React 版本运行 npm ls react;单体仓库和链接包会无声复制 React
“对象不能作为 React 子节点”渲染 {user}{date} 而非具体字段渲染原始值:{user.name}{date.toISOString()}
“列表中的每个子节点都应有唯一 key”缺失或重复的 key使用 key={item.id}(规则 4);重复 key 表示数据本身有重复 id —— 修复数据源
“水合失败” / “文本内容不匹配”服务端与客户端渲染出不同标记参见 debug.md 中的链式排查 —— 日期/随机数/本地化、无效 HTML 嵌套、浏览器扩展
“无法在渲染另一个组件时更新组件”在渲染期间对另一组件调用 setState将 setState 移入事件处理器或副作用中
压缩后的 React 错误 #NNN生产构建会移除提示信息在 react.dev/errors/NNN 解码,然后在开发环境重现

组件规则

export function UserCard({ user, onEdit }: UserCardProps) {
  // 1. 钩子必须放在最前,且无条件
  const [isOpen, setIsOpen] = useState(false)
  // 2. 衍生值应在渲染中计算 —— 永远不要放在副作用中
  const fullName = `${user.firstName} ${user.lastName}`
  // 3. 事件处理器
  const handleEdit = () => onEdit(user.id)
  // 4. 提前返回必须位于所有钩子之后
  if (!user.isVisible) return null
  // 5. JSX
  return (...)
}

导出 props 接口(参见 typescript.md 了解变体 props、泛型、事件类型)。提前返回必须位于所有钩子调用之后 —— 若在钩子前返回,会导致渲染间钩子顺序不一致,引发崩溃。

状态管理

数据是否来自 API?
├─ 是 → 使用 TanStack Query(非 Redux、非 Zustand、非 Context)
└─ 否 → 是否需要在刷新后保留?或跨组件共享?
    └─ 是 → 使用 URL searchParams
        └─ 否 → 是否跨组件共享?
            ├─ 是 → Zustand(频繁变化)或 Context(极少变化)
            └─ 否 → 使用 useState

核心默认配置(完整模式、分页、乐观更新、持久化:参见 state.md):

  • TanStack Query >=5staleTime 默认为 0 —— 每次挂载和窗口聚焦都会重新获取数据。按资源设置 staleTime:用户可容忍的数据过期时间。保持 gcTime(默认 5 分钟) ≥ staleTime。查询键应来自统一的键工厂,确保唯一来源。
  • Zustand >=5:通过选择器读取状态 —— 直接使用 useStore() 会在每次 store 变化时触发重渲染;若选择器返回新对象,需使用 useShallow。每个关注点一个 store —— 避免创建“上帝 store”,否则等同于 Redux 但缺少开发者工具。
  • Context:每次值变化都会使所有消费者重渲染,无选择器机制。应通过 memo 包装值对象;将状态与分发逻辑拆分为两个 Context,以避免仅需 setter 的消费者被意外重渲染。

React 19

  • 服务端组件 在服务端渲染,不发送任何 JS;客户端组件是交互性的叶子节点。序列化规则、流式传输、服务端动作:参见 server-components.md
  • **use(promise)** 会暂停直到 Promise 解析,且可在条件中调用 —— 这是少数例外于钩子规则的情况。绝不能在客户端渲染中创建新 Promise(每次渲染都会创建新实例,导致永久挂起):应在父组件创建或缓存。
  • **ref 现在是普通属性** —— 不再需要 forwardRef。**<Context>** 会直接渲染为提供者。旧模式迁移:参见 migration.md
  • 动作(Actions)useActionState(action, initial) 返回 [state, formAction, pending]useFormStatus 可从任意子元素读取 pending 状态 —— 无需向提交按钮传递 prop;useOptimistic 自动渲染乐观值并在抛出时回滚。完整表单示例:参见 forms.md

性能优化

操作优先级 —— 先测量再动手(工作流参见 performance.md):

优先级技术适用场景
P0路由级代码分割(lazy + Suspense)始终使用 —— 用户仅支付其访问路由的成本
P0并行数据加载(Promise.all、预加载)任意页面若有 2 个以上独立请求 —— 水瀑布效应主导加载时间
P1虚拟化长列表数千行或数百个复杂行 —— 通过性能分析确认,DOM 节点数量是主要成本
P1useDeferredValue / startTransition输入或拖拽时卡顿,因重型子树每输入一次就重渲染
P2memo / useMemo(基于性能分析证据)仅在未启用 React 编译器时使用(→ 核心规则 6)

输出校验

在输出组件或审查结论前,请确认:

  • 所有列表的 key 是否为稳定标识?是否在可排序数据中使用了索引?
  • 所有 useEffect 是否明确关联外部系统?可推导的值是否在渲染中计算?
  • 'use client' 是否仅用于交互性叶子组件?是否有服务端导入跨越边界?
  • 状态变更是否有效更新或使对应查询键失效?
  • 交互元素是否为原生标签(buttonalabel)并具备可访问名称?
  • 错误路径是否已渲染?是否存在错误边界或显式错误状态,而非仅处理正常流程?
  • 新抽象是否已有第二个消费者?或仍保持具体?

配置项

用户自定义变量。默认值在用户声明偏好前生效;存储于 ~/Clawic/data/react/config.yaml

变量类型默认值效果
frameworknextjs \vite \remix
stylingtailwind \css-modules \styled-components
state_libraryzustand \redux-toolkit \jotai
package_managernpm \pnpm \yarn \

用户可记录以下偏好领域:

  • 工具链 —— 测试运行器(Vitest/Jest)、lint/format 工具链、monorepo 结构 —— 影响 testing.md 示例与 scaffold 命令
  • 约定 —— 文件命名、目录结构、超出命名导出规则的组件模式 —— 影响生成文件布局
  • 平台 —— SSR 要求、目标浏览器、部署目标 —— 影响水合指导与性能预算
  • 安全策略 —— 审查时对遗留模式和 AI 错误的警告强度(重写 vs 注释) —— 影响审查行为

项目级事实(React 版本、编译器开关、技术栈偏差)应记录在 memory.md,而非配置文件 —— 因其随项目变化,而非用户偏好。

常见陷阱

陷阱为何失败正确做法
{count && <X />}0 为假值但可渲染 —— 页面会显示字面量 0改为 {count > 0 && <X />}
key={index} 用于可排序/过滤的列表状态与 DOM 绑定位置而非项目本身 —— 输入框显示错误值使用 key={item.id}
在副作用依赖项中使用对象/数组字面量每次渲染生成新引用 → 副作用每次执行依赖原始值,或上游记忆化
修改状态后立即调用 setState引用相同 → React 跳过重渲染创建新引用:setArray([...array, item])
在副作用中获取数据但无取消机制旧值响应慢,覆盖新值响应快在清理函数中使用 AbortController(如下)
“我的副作用执行两次” → 删除 StrictMode双重挂载仅为开发环境且有意为之:暴露缺失清理逻辑修复清理逻辑;保留 StrictMode
期望错误边界捕获处理程序/异步错误错误边界仅捕获渲染与生命周期错误在处理程序中使用 try/catch;在突变中使用 onError
水合不匹配Date.now()Math.random()、本地化格式、typeof window 分支在服务端与客户端渲染不同移至副作用中,或对单个元素使用 suppressHydrationWarning
在渲染期间调用 setState渲染 → setState → 渲染,形成无限循环在渲染中直接计算衍生值;setState 放入事件处理器或副作用中
useEffect(async () => …)副作用必须返回清理函数或无返回,不能返回 Promise在内部定义异步函数并调用
在 setState 后立即读取状态状态是每渲染周期的快照 —— 变量仍持有旧值使用传入的值,或使用函数式更新;副作用响应下一渲染
// 取消机制 —— 竞态条件修复
useEffect(() => {
  const controller = new AbortController()
  fetch(url, { signal: controller.signal })
    .then((r) => r.json())
    .then(setData)
    .catch((e) => { if (e.name !== 'AbortError') setError(e) })
  return () => controller.abort()
}, [url])

AI 生成代码的常见错误

审查 AI 生成代码时需重点检查以下模式:

错误正确模式
使用 useEffect + useState 推导值在渲染中直接计算(→ 核心规则 2)
在 useEffect 中获取数据使用 TanStack Query,或在 Suspense 下使用 promise + use()
使用 Redux/Context 管理 API 数据使用 TanStack Query —— 服务端状态是缓存,不是应用状态
使用默认导出使用命名导出(→ 核心规则 5)
对所有内容使用 useCallback/useMemo编译器开启时:无需手动添加;关闭时:需先经性能分析器验证(→ 核心规则 6)
第一次使用即创建通用抽象(如 <DataTable> 仅用于一个表格)先写具体实现;在第二个真实消费者出现时再抽象
组件过大在约 50 行 JSX 或 300 行文件处拆分(→ 核心规则 8)
任意位置均无错误边界在路由级别和每个高风险功能处添加
any 消除严格模式警告修复类型;真正动态时使用 unknown 并进行类型缩小(参见 typescript.md
使用 <div onClick> 实现交互元素使用原生 button/a —— 键盘与焦点支持自动获得(参见 accessibility.md
测试实现细节(状态值、mock 调用次数)测试用户可见的行为:角色、标签、可见输出(参见 testing.md

专家争议点

  • 默认记忆化 vs 分析先行。React 编译器已解决新代码的分歧(不写手动 memo)。对于编译器前的代码库:库和设计系统可防御性地记忆化导出(未知消费者);应用应先分析再决定。
  • RSC 全局 vs SPA。内容与电商类站点采用服务端组件(减少包体积、利于 SEO)更具优势。仪表板密集型应用则需权衡。
I
@ivangdavila

已收录 28 个 Skill

相关推荐