关于 react-stinky
React Stinky 是用于审查 React 和 TypeScript 代码异味的 skill,检查可维护性、正确性、无障碍和类型问题,并给出严重程度、影响及具体修复建议。
主要功能
- 01检查九类 React 可维护性问题
- 02发现状态与 effect 问题
- 03审查无障碍和渲染正确性
- 04标记 TypeScript 与异步处理问题
- 05目录或仓库范围内检查跨文件重复
- 06报告严重程度、代价和修复建议
适用场景
- 01重构前审查 React 组件
- 02发现 effect 与状态设计问题
- 03检查 UI 标记中的无障碍问题
- 04扫描仓库中的重复组件或工具
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ saschb2b/ skills --skill react-stinky安装后在 Claude Code 中输入/react-stinky确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
CARD.manifest.jsonCARD.mdCARD.md.sigstoreCARD.svgpermissions.yamlreferences/catalog.mdduplication-pass.mdindex.mdlog.mdrestructure-pass.mdreferences/concepts/composition-layer.mdcontrolled-uncontrolled.mdeffects-model.mdhydration.mdimpossible-states.mdindex.mdone-fact-one-home.mdrender-snapshots.md详细介绍
React Stinky
一款面向 React 和 TypeScript 的全面代码异味检测工具。它会找出让组件、Hook 或模块难以阅读、推理和修改的模式,解释每种模式的代价,并提出具体的修复方案。检测范围涵盖属性和 API 设计、状态与数据流、Effect 和生命周期、组件结构、渲染正确性、无障碍性、异步与错误处理,以及 TypeScript 规范。完整目录包含检测信号、修复方法、例外情况和来源,见 catalog.md;运行扫描前请先阅读。
它会将两个相邻领域交由相关技能处理,以避免重复:将记忆化(useMemo、useCallback、React.memo)交由 react-compiler,将颜色字面量交由 theme-colors。如果未安装这些技能,请用一行注明发现的问题,然后继续。除此之外,日常 React 可维护性的所有方面都在涵盖范围内。
它会检测什么
九大支柱。每类下的类别及其检测信号和来源见 catalog.md。这些发现所依据的背景模型(受控与非受控、渲染快照、将副作用视为同步、一个事实只有一个归属、不可实现的状态、组合层、hydration)是 references/concepts/ 下单独链接的概念;当某项发现需要解释其底层模型,而不只是点出名称时,再阅读对应概念。
- 组件 API 和 props(基础,共 18 类)。组件和 prop 的命名、布尔值和回调约定、优先使用字符串联合变体而非布尔标志、判别联合、受控与非受控状态、children 和插槽组合、渲染 prop、泛型、扩展 HTML、refs、样式 API、无障碍 props、服务端组件边界、JSDoc。
- 状态和数据流。 将可推导值存入
useState、将 props 复制到状态中、同一事实存在两个数据源、prop 层层传递、定时器和异步后续操作中的过期闭包、混淆useState/useRef的用途、臃肿的 context 和不稳定的 provider 值。 - 副作用和生命周期。 用副作用计算派生数据、替代事件处理程序或将数据向上推送给父组件;fetch、订阅和定时器没有清理(竞态和泄漏);依赖数组与副作用读取的内容不匹配;用副作用而不是
key重置状态。 - 组件结构和 hooks。 同时负责获取数据、处理逻辑和呈现的臃肿组件(以及臃肿 hooks);与环境耦合的视图,其中的数据访问和领域规则本应属于更上层,并应作为以行为命名的 props 传入(判断标准是:能否在没有 mock 的情况下于 Storybook 中渲染);在另一个组件内定义组件(每次渲染都会重新挂载);适合提取为自定义 hook 的有状态逻辑;条件式调用 hooks;hook 或工具函数的位置参数泛滥。
- 渲染正确性。 对会重新排序或编辑的列表使用数组索引作为
key;直接修改状态或 props;非纯渲染(随机数和读取时钟、在渲染期间调用setState、不利于 hydration 的浏览器读取);被&&意外渲染出来的0;嵌套三元表达式;复制粘贴的 JSX 代码块。 - 标记中的无障碍性。 在非交互元素上使用
onClick,却没有role、tabIndex或键盘处理程序;应使用语义化元素却堆叠 div;表单控件没有关联的标签;手动实现的模态框和菜单中的焦点管理有误。 - 异步、事件和错误处理。 吞掉错误或允许重复提交的异步处理程序;有风险的子树周围缺少错误边界;未清理的 HTML 和 URL 接收点(
dangerouslySetInnerHTML、javascript:href)。 - TypeScript 规范。
any、as any和@ts-ignore;具有误导性的as类型断言和非空断言!;宽泛的内部类型(object、Function、用字符串表示的枚举);未标注类型的 catch 块;切换联合类型时未检查穷尽性。 - 跨文件重复(仅限文件夹和仓库范围)。已有可复用组件,却在其他位置内联重写;同一个 hook 或工具函数在多个文件中重复复制;在两个位置声明同一个类型。方法见 duplication-pass.md。
检查范围模式
根据请求匹配检查范围,然后对其运行以下工作流。
| 模式 | 触发条件 | 扫描内容 |
|---|---|---|
| 仓库全面检查 | “检查代码库是否有异味” | 使用 Glob 扫描 **/*.tsx,以及 hook 和模块 .ts 文件(use-*.ts、lib/)。除非另有要求,否则跳过 node_modules、构建输出、生成代码、*.test.*、*.stories.*。优先检查共享目录、组件目录、hook 目录和 ui 目录,以及导出的符号。 |
| 文件夹扫描 | 指定一个或多个目录 | 同上,但范围限定为这些目录。 |
| 文件扫描 | 指定具体文件 | 完整阅读每个文件。检查每个组件、hook、prop 接口和导出函数。 |
| 片段嗅查 | 粘贴的函数或组件,或指定的一个符号 | 只检查该部分。说明对任何未展示内容所作的假设。 |
文件夹和仓库全面检查范围还要执行跨文件重复检查 (duplication-pass.md),以发现单文件扫描无法发现的 DRY 异味:在其他位置重新内联实现的组件,或跨文件复制粘贴的同一 hook 或类型。单文件和片段范围无法检查这一点,因此应说明未检查跨文件重复,而不能暗示代码没有重复。
每个目标的工作流
- 找出范围内的组件、hook、prop 接口和模块。
- 根据目录中针对每个文件的支柱(1 到 8)逐一检查。
- 报告前先应用对应的“不要标记”条目。如果适用,则不报告该问题。
- 为异味评级(见下文)。
- 给出问题发现,包含位置、代价,以及从现状到修复方案的对比。
- 在文件夹或仓库范围内,完成逐文件检查后,执行跨文件重复检查(支柱 9,duplication-pass.md),并将其发现纳入结果。
- 如果问题集中在结构性异味(重复状态、prop 逐层传递、通过 effect 将数据传给父组件、上帝组件、视图耦合),或请求是重构、解耦或让组件可复用,则使用 restructure-pass.md 中的依赖图方法规划修复:将每种关系映射为带标签的边,然后重新连接,使单层边尽可能少,再提出代码方案。
- 最后给出问题数量摘要。如果没有问题通过筛查,则说明代码状态良好。
异味评级
- 腐臭。 存在 bug,或正确性、可访问性或服务端边界方面的问题。立即修复。(控件缺少
aria-label或键盘处理器、跨越 RSC 边界的函数、value || 50导致value={0}被忽略、状态数组被修改、条件式 hook、复制到状态后会不一致的 props、{count && ...}渲染出多余的0、会导致 hydration 出错的Date.now()ID、未经清理的dangerouslySetInnerHTML。) - 怪味。 确实拖累可维护性,但不是 bug。应该修复。(本应改用一个联合类型的布尔值泛滥、上帝组件、用于计算派生数据的 effect、没有 pending 或错误状态的异步保存、本应改用复合组件的配置数组。)
- 淡味。 轻微或仅涉及风格。可选修复。(自定义 prop 仅使用
loading、宽泛的Record<string, unknown>sx 类型、只是重复名称的 JSDoc。)
不要标记(让这项检查保持实用的守则)
目录为每种代码异味都列出了一条例外说明。这些说明适用于所有代码异味。请遵守,否则这项技能就会变成一种麻烦。
- 原生 HTML 属性名称保持原样。不要给
disabled、required、checked、open添加is前缀,也不要重命名原生<input>上的onChange。 - 已确立的库惯例不属于代码异味。遵循文件中已有的库惯例(MUI 的
open、slots、sx;Radix 的asChild)。 - 对于数据驱动、布局固定的组件(例如数据网格),使用配置对象属性是正确的。
- 对真正的外部同步(订阅、非 React 小部件、浏览器 API),使用 effect 是正确的做法。只有在 effect 用来计算派生数据或替代事件处理程序时才标记。
- 当你有意为状态设置初始值,并且名称能体现这一点(
defaultValue)时,将属性复制到状态中没有问题。只有在预期属性后续变化会更新状态时才标记。 - 对于永不重新排序、插入或删除的静态列表,使用索引作为键没有问题。
- 在另一个组件内部定义组件,用作小型、无状态的渲染辅助函数没有问题。如果它持有状态、经过 memoization 或并非小组件,则应标记。
- 在确实没有类型的边界处使用
any有时是权宜之计。优先使用unknown并进行缩窄,但不要因此阻止合并。 - 每个实际问题只报告一次。优先采用能消除代码异味的最小改动。与目录默认规范相比,应尊重一致的本地惯例。
- 将 memoization 交由
react-compiler处理,将颜色字面量交由theme-colors处理,避免重复处理。
输出格式
React Stinky report, <scope>
src/components/SeedRow.tsx
[Rancid] clickable-nonsemantic (a11y markup), line 297
Smell: a <Stack> (renders a div) has onClick but no role, tabIndex, or keyboard handler.
Cost: keyboard and screen-reader users cannot trigger it; it is invisible to assistive tech.
Fix: render a real control (component="button" or an IconButton), or add
role="button" tabIndex={0} and an onKeyDown for Enter and Space.
Source: MDN button role (https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/button_role)
[Funky] effect-for-derived (state and effects), line 40
Smell: a useEffect plus setState computes `fullName` from `first` and `last`.
Cost: an extra render and a state value that can drift from its inputs.
Fix: compute during render, `const fullName = `${first} ${last}``. Delete the effect and the state.
Source: React, You Might Not Need an Effect (https://react.dev/learn/you-might-not-need-an-effect)
Summary: 1 rancid, 1 funky across 1 file.
如果范围内没有问题,请直接说明:“代码异味已清除。在 <scope> 中未发现可维护性问题。”
来源
18 个组件 API 类别(支柱 1)提炼自 cant-maintain React API 设计挑战集,每个类别都可追溯至 React、TypeScript、MDN、Next.js 或 MUI 的来源。其余支柱将同一可维护性视角扩展到状态、effect、结构、渲染、无障碍、异步与错误处理以及类型,每项均取材自 catalog.md 中的 React 官方文档、MDN 或 TypeScript 手册。