学习如何使用 Claude Code 通过表征测试、小步、安全检查与状态理清来重构 React 组件,在不改变行为的前提下改进结构。

React 重构感觉很冒险,原因是大多数组件并不是干净的小模块,而是混着 UI、状态、effects 和“再加一个 prop”修补的活着的代码堆。改结构时,你往往会无意中改变时序、身份或数据流。
重构最常在无意中改变行为的情况包括:
key 改变导致状态被重置。当“清理”与“改进”混在一起时,重构很快就变成重写。你先是抽出一个组件,然后重命名一堆东西,再“修”状态结构,然后替换一个 hook。很快你就在改布局的同时改逻辑。没有保护措施时,很难判断哪个改动引入了 bug。
一次安全的重构有一个简单承诺:用户行为不变,同时代码更清晰。Props、事件、加载状态、错误状态和边界情况都应保持不变。如果行为需要改变,应是有意的、小范围的并且明确说明。
如果你使用 Claude Code(或任何编程助手)来重构 React 组件,把它当成一个速度快的结对程序员,而不是自动驾驶。让它在修改前描述风险,提出小步计划,并说明它如何验证行为保持不变。然后你自己再验证:运行应用,点击那些奇怪的路径,并依靠能捕获组件当前行为(而不是你希望它如何表现)的测试。
挑一个实际让你浪费时间的组件。不是整个页面,不是“UI 层”,也不是模糊的“清理”。选一个难读、难改或充满脆弱状态与副作用的单一组件。明确的目标也会让助手的建议更容易验证。
把目标写成能在五分钟内检查的句子。好的目标关注结构,而不是结果:“拆成更小的组件”、“让状态更容易跟踪”或“在不用 mock 半个应用的情况下可测试”。避免“让它更好”或“提升性能”这样的模糊目标,除非你有明确的指标和已知瓶颈。
在打开编辑器前先设定边界。最安全的重构往往很无趣:
然后列出那些在移动代码时可能悄悄破坏行为的依赖:API 调用、context 提供者、路由参数、功能开关、分析事件和共享全局状态。
一个具体例子:你有一个 600 行的 OrdersTable,它负责获取数据、过滤、管理选择并显示带详情的抽屉(drawer)。一个清晰的目标可以是:“把行渲染和抽屉 UI 抽成组件,把选择状态搬到一个 reducer 中,UI 不发生变化。”这个目标告诉你“完成”是什么样子,也限定了范围。
在重构之前,把组件当作黑盒。你的工作是捕捉它现在的表现,而不是你希望它将来的样子。这能防止重构变成重新设计。
首先用简单语言写下当前行为:在这些输入下,UI 会展示那些输出。包括 props、URL 参数、功能开关,以及来自 context 或 store 的任何数据。如果你用 Claude Code,粘贴一个小而集中的代码片段,让它把行为用精确的句子复述一遍以便后续检查。
覆盖用户实际看到的 UI 状态。组件在“顺利路径”看起来没问题,但在遇到 loading、empty 或 error 状态时可能会崩溃。
还要记录那些容易被忽略但会导致重构失败的隐式规则:
示例:你有一个用户表,加载结果、支持搜索并按“Last active”排序。写下在搜索为空时、API 返回空列表时、API 出错时、两个用户有相同“Last active”时间时发生的事情。注意小细节,比如排序是否不区分大小写,以及在过滤变化时表格是否保留当前页。
当你的笔记变得枯燥且具体时,你就准备好了。
表征测试(characterization tests)是“它现在就是这样”的测试。即便行为很奇怪或不一致,也要把它们描述出来。听起来反直觉,但它能防止重构悄悄变成重写。
在用 Claude Code 重构时,这些测试是你的安全栏杆。工具可以重塑代码,但你来决定什么必须保持不变。
关注用户(和其他代码)依赖的点:
为了让测试稳定,断言结果而非实现。偏好“保存按钮变为禁用并显示一条消息”而不是“调用了 setState”或“这个 hook 被运行了”。如果测试因为你重命名了组件或重排了 hook 而失败,它没有保护到真正的行为。
异步行为通常是重构改变时序的地方,要明确处理:等待 UI 稳定后再断言。如果有定时器(防抖搜索、延迟提示),使用伪造定时器并推进时间。如果有网络调用,mock fetch,断言成功和失败后用户所见。对于类似 Suspense 的流程,测试 fallback 和解析后的视图。
示例:一个“Users”表在搜索完成后才显示“No results”。表征测试应该锁定这个序列:先显示 loading 指示器,然后展示行或空信息,无论你后来如何拆分组件。
目标不是“更大更快地改动”,而是清晰地知道组件在做什么,然后一次只改一小点,同时保持行为稳定。
先粘贴组件并让它用白话总结职责。追问细节:显示了哪些数据、处理了哪些用户动作、触发了哪些副作用(获取、定时器、订阅、analytics)。这通常会暴露那些让重构风险上升的隐藏职责。
接着让它列出依赖地图。你需要一个输入输出清单:props、context 读取、自定义 hook、本地 state、派生值、effects 以及任何模块级助手。一个有用的地图还会指出哪些是可以安全移动的(纯计算),哪些是“粘性的”(时序、DOM、网络)。
然后让它提出抽取候选,但遵守一条严格规则:把纯展示部分和有状态控制部分分开。大量 JSX 且只依赖 props 的部分是优先抽取对象。混合了事件处理、异步调用和状态更新的部分通常不是好抽取对象。
一个稳妥的工作流:
检查点很重要。让 Claude Code 给出一个最小计划,每一步都能提交并回退。一个实用的检查点示例是:先“抽出 <TableHeader> 且不改逻辑”,再去动排序状态。
具体例子:如果组件渲染客户表、控制过滤并获取数据,先抽出表格标记(表头、行、空状态)为纯组件。之后再移动过滤状态或 fetch effect。这样的顺序能把 bug 留在 JSX 之外。
拆分大组件时,风险不在于移动 JSX,而在于无意中改变数据流、时序或事件连线。把抽取当成先复制并接线的操作,清理放在后面。
从 UI 中已经存在的边界入手,而不是文件结构中的边界。找那些你能用一句话描述为独立“事物”的部分:带操作的头部、过滤栏、结果列表、页脚带分页、带有对话框的部分。
安全的第一步是抽取纯展示组件:props 进,JSX 出。刻意让它们无聊:不加新状态、不加 effects、不发新请求。如果原组件有一个点击处理器做了三件事,把处理器保留在父组件并传下去。
通常可行的安全边界包括头部区域、列表与行项、只含输入的过滤器、页脚控制(分页、总计、批量操作)以及对话框(打开/关闭和回调通过 props 传入)。
命名比想象中重要。使用具体名字,如 UsersTableHeader 或 InvoiceRowActions。避免像“Utils”或“HelperComponent”这样的笼统名字,因为它们掩盖职责并容易把关注点混在一起。
只有在确有需要时才引入容器组件:当一块 UI 必须拥有状态或 effects 才能保持一致时才做。即便如此,也要窄小。一个好的容器只负责一件事(比如“过滤状态”),把其他内容通过 props 传下去。
混乱组件通常把三类数据混在一起:真实的 UI 状态(用户改动的内容)、派生数据(可计算得出)和服务端状态(来自网络)。如果你把它们都当作本地 state 处理,重构会风险增加,因为你可能无意中改变更新时机。
先给每个数据项贴标签:用户是否会编辑它?能否从 props、state 和抓取的数据计算出来?这个值是这里拥有的,还是只是穿透传递?
分离状态与派生值
派生值不应该放在 useState 里。把它们移到小函数里,或者在开销较大时用 memoized selector。这样能减少 state 更新并让行为更可预测。
一个安全模式:
useState。useMemo 包裹昂贵计算。让 effects 保持单一且具体
当 effect 做太多或依赖错误时,会破坏行为。目标是“每个目的一个 effect”:一个同步到 localStorage、一个用于抓取、一个用于订阅。如果一个 effect 读取很多值,通常它在隐藏额外职责。
如果你用 Claude Code,要求一个微小改动:把一个 effect 拆成两个,或把一项职责移动到 helper。每做一步就运行表征测试。
注意 prop drilling。用 context 替代只有在它减少重复 wiring 并且能澄清所有权时才合适。一个好迹象是 context 表示一个应用级概念(当前用户、主题、功能开关),而不是某个组件树的临时变通方案。
示例:表格组件可能同时把 rows 和 filteredRows 存在 state 中。保留 rows 为 state,从 rows + query 计算 filteredRows,并把过滤逻辑放在纯函数里,便于测试且不易出错。
重构出问题最常见的原因是改得太多才发现。解决办法很简单:以微小检查点工作,把每个检查点当作迷你发布。即便在一个分支上,也要让改动保持 PR 大小,以便快速看出哪里出问题。
在每次有意义的改动后(抽取组件、改变状态流、清理 effect),停下来证明你没有改变行为。证据可以自动化(测试)也可以手动(浏览器快速检查)。目标不是完美,而是快速发现问题。
一个实用的检查循环:
如果你在用像 Koder.ai 之类的平台,快照和回滚可以在你迭代时当作安全栏杆。你仍然需要正常提交,但快照在比较“已知良好”版本和当前实验状态时很有用。
随手保持一个简单的行为日志,记录你验证过的要点,防止重复检查相同内容。
例如:
当出现问题时,日志告诉你要再次检查什么,检查点使回退成本很低。
大多数重构失败于一些小而无聊的地方。UI 看起来还能用,但某个间距规则没了,点击处理器触发了两次,或者列表在输入时丢失焦点。助手可能让代码变得更整洁,但行为悄悄漂移时你反而更难发现。
一个常见原因是结构改变。你抽了组件并包了一层额外的 \u003cdiv\u003e,或把 \u003cbutton\u003e 换成可点击的 \u003cdiv\u003e。CSS 选择器、布局、键盘导航和测试查询都可能因此改变而无人察觉。
最常导致行为破坏的陷阱:
{} 或 () => {})会触发额外重渲染并重置子组件状态。关注那些原本稳定的 props。useEffect、useMemo 或 useCallback 中可能引入过期值或循环。如果某个 effect 以前是“在点击时运行”,不要把它改成“当任何东西变化时运行”。具体例子:拆分表格组件并把行的 key 从 ID 改为数组索引貌似正常,但在行重排时会破坏选择状态。把“干净”看作附加好处,把“行为不变”作为必须达成的条件。
在合并前,你需要能证明重构保持了行为。一些简单直接的信号:原有测试不需要改动就通过,你的新表征测试也通过。
在最后一次小改动后做这个快速检查:
onChange 仍在用户输入时触发,而不是在 mount 时)。一个快速的健全性检查:打开组件并做一个奇怪流程,比如触发错误、重试然后清除过滤。重构常在这些转换流程中出问题,即便主路径还行。
如果任何项失败,回退上一个改动并用更小的步骤重做,这通常比调试一个大 diff 更快。
设想有个 ProductTable 组件什么都做:抓取数据、管理过滤、控制分页、打开删除确认对话框并处理行操作(编辑、复制、归档)。它从小变成了一个 900 行的文件。
症状很熟悉:状态散落在多个 useState,几个 useEffect 以特定顺序触发,微小改动会在某些过滤激活时破坏分页。大家开始不敢碰它,因为感觉不可预测。
在改变结构之前,用几个 React 表征测试锁定行为。关注用户行为,而不是内部 state:
现在你可以以小提交重构了。一个干净的抽取计划可能是:FilterBar 渲染控制项并发出过滤变化;TableView 渲染行和分页;RowActions 管理操作菜单和确认对话框的 UI;useProductTable hook 管理混乱的逻辑(查询参数、派生状态和副作用)。
顺序很重要。先抽取傻瓜式 UI(TableView、FilterBar),通过不变的 props 传递。把危险的部分留到最后:把状态和 effects 移到 useProductTable。在那一步保持旧的 prop 名称和事件形状,以便测试继续通过。如果测试失败,你发现的是行为变化,而不是样式差异。
如果你想让使用 Claude Code 重构 React 组件每次都感觉安全,把刚做过的事情做成一个小模板。目标不是增加流程,而是减少惊讶。
保持一个简单的重构模板
写一份短流程以便在任何组件上遵循,即使疲惫或着急也能用:
把它存为笔记或仓库片段,下次重构可以直接用同一套安全栏杆。
决定在行为锁定后做什么
组件稳定且更易读后,根据用户影响决定下一步。常见顺序是:先无障碍(标签、焦点、键盘),再性能(memo、昂贵渲染),最后清理(类型、命名、死代码)。不要在一个 PR 中混合这三项。
如果你用像 Koder.ai(koder.ai)这样的氛围式编码工作流,规划模式能帮你在动手前列出步骤,快照与回滚可作为你迭代时的检查点。完成后导出源码便于审查最终 diff 并保持干净历史。
知道何时停止并交付
当测试覆盖了你最怕破坏的行为、下一步改动会引入新功能,或你想在同一 PR 中把命名、类型与架构都做到完美时,就该停止重构并交付。如果拆分大表单已去除了纠缠状态且测试覆盖验证了校验与提交,合并它。把剩余想法列为简短待办以便以后处理。
React 重构经常无意中改变对象标识和执行时序,因此出问题而你却没注意到。常见的行为破坏包括:
key 变化导致状态重置。在测试证明之前,假定结构性改动可能改变行为。
使用一个紧凑且可快速校验的目标,专注于结构而不是泛泛的“改进”。好的目标示例:
避免诸如“让它更好”之类的模糊目标,除非你有明确的指标和已知瓶颈。
把组件当作黑盒,写下用户可观察到的行为:
当你的笔记变得枯燥且具体时,就是有用的记录。
添加描述组件当前行为的表征测试(characterization tests),即便行为奇怪也要捕捉。
优先覆盖:
断言应该针对 UI 的结果,而不是内部实现细节。
把助手当成谨慎的结对程序员来用:
不要接受一次性“大重写”的大 diff,推进可验证的小改动。
先抽取纯展示型部分:
先做复制并接线(copy-and-wire),后做清理;在 UI 安全拆分后再处理状态和副作用。
使用与真实身份绑定的稳定 key(例如 ID),而不是数组索引。
索引 key 在排序、过滤、插入或删除行时会导致 React 错误复用实例,常见问题有:
如果重构更改了 key,这是一项高风险改动,需要针对重排序场景测试。
尽量不要把可推导的数据放到 useState。
安全做法:
rows + query 等输入中计算派生数据(如 filteredRows)用检查点把每一步变成可回退的最小改动:
如果你用 Koder.ai,快照和回滚能在实验出问题时提供额外保障。
当行为被锁定且代码明显更易于修改时就该停止:
把剩下的访问性、性能和清理工作作为后续任务记录下来。
useMemo这样能减少不必要的更新并让行为更可预测。