问题修复
当 React 组件 props 因 Storybook…
在 React 组件类型被 story、mock、测试或 demo 放宽后,按真实非测试调用点收窄 props 类型,并让支持代码适配严格契约。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
当 React 组件 props 因 S通用
解决思路
- 在修改类型前,先搜索并找到实际非测试、非 Storybook 的调用点。
- 将线上代码路径(live code paths)视为 prop 契约的唯一事实来源。
- 不要仅因为能让 Storybook、测试或 mock 数据更容易编写,就保留可选 props。
- 只有在存在非 Storybook、非测试调用点未提供该 prop,且该调用点有不提供的合理原因时,才保留该 prop 为可选。
- 类型不应允许表达非测试、非 Storybook 调用点中未观察到的状态。
- 类型应尽可能严格,以便组件代码尽可能简单。
- 尽可能从现有线上代码路径的值和 API 派生、提取类型。
- 寻找可疑组件:大 props interface 且很多可选字段。
- 寻找可疑组件:可选回调调用,如 `onSelect?.(...)` 或 `onArchive?.(...)`。
- 寻找可疑组件:针对线上代码很可能总会提供的值使用兜底状态处理,如 `items ?? []`、`count ?? 0`、`handler && ...`。
- 寻找可疑组件:UI affordance 总是渲染,但其回调是可选的。
- 寻找可疑组件:看起来面向 demo 的 props,如 `defaultFoo`、替代 handler 形状、线上代码未使用的显示开关。
- 不要仅凭 story 或 test 选择目标;stories/tests 只能作为类型被放宽的辅助证据,不能作为某状态真实存在的证据。
- 搜索组件、导出的 prop 类型以及共享子 primitive 的所有 import/使用处。
- 将调用点分类为线上代码路径或支持代码:线上代码路径包括 app routes、wired components、providers、hooks、生产包导出,以及这些路径使用的共享组件。
- 将调用点分类为线上代码路径或支持代码:支持代码包括 Storybook stories、测试文件、fixtures、mocks、demo harnesses 和仅用于视觉展示的示例。
- 只让线上代码路径决定组件 API 支持什么。
- 阅读线上调用点并逐个分类 prop:Required 表示每个非测试、非 Storybook 调用点都提供它。
- 阅读线上调用点并逐个分类 prop:Optional 表示至少一个非测试、非 Storybook 调用点省略它,且该省略是有意义的运行时状态。
- 阅读线上调用点并逐个分类 prop:Removed 表示没有任何非测试、非 Storybook 调用点使用它。
- 区分 nullability 与 optionality;如果线上代码总是传 prop 但值可为空,优先使用必需可空 prop,如 `focusedItem: FocusedItem | null`,而不是 `focusedItem?: FocusedItem | null`。
- 更新导出的 prop 类型,使其只匹配线上代码路径中观察到的状态。
- 认识到类型越宽松、越可选,组件需要考虑的可能状态越多;每个可选 prop 都会产生额外分支,需要处理、测试并保持正确。
- 优先使用能阻止不可能状态的严格类型,而不是需要防御性渲染逻辑的宽泛类型。
- 如果组件总是渲染某个交互 affordance,就要求提供让该交互生效的 handler;不要允许惰性状态,例如可见 menu item 调用 `onRename?.(...)`。
- 优先从线上 API 派生类型,而不是手动重述:用 `Parameters<typeof fn>[0]` 表示函数参数类型。
- 优先从线上 API 派生类型,而不是手动重述:用 `ReturnType<typeof fn>` 表示返回类型。
- 优先从线上 API 派生类型,而不是手动重述:用 `Extract<Union, Shape>` 将 union 收窄到真实变体。
- 优先从线上 API 派生类型,而不是手动重述:用 `React.Dispatch<React.SetStateAction<T>>` 表示 React state setter,而不是近似为 `(value: T) => void`。
- 当推断会拓宽或掩盖目标状态形状时,优先使用显式状态类型参数,例如 `const [dialogState, setDialogState] = useState<DialogState>({ id: null, isOpen: false })`。
- 避免依赖隐式 `useState(...)` 推断,当它产生宽泛的可空对象形状、字符串字面量拓宽,或之后需要手写近似的回调类型时。
- 不要止步于导出组件:如果它把宽泛 props 传给子 primitive,也要继续收窄。
- 如果 row/menu/button 子组件接收可选 handler 只是因为父组件 props 宽泛,就收窄这些内部 props。
- 将可选调用 `onRename?.(id, name)` 替换为必需调用 `onRename(id, name)`。
- props 变为必需后,移除只为被拓宽类型而存在的防御性兜底逻辑。
- 将 `new Set(expandedIds ?? defaultExpandedIds ?? [])` 改为 `new Set(expandedIds)`。
- 将 `items && items.length > 0` 改为 `items.length > 0`。
- 如果多个组件共享同一个宽泛 prop 类型,一起更新它们,使它们都强制同一套线上代码路径契约。
- 如果 story 或 test 在收窄 props 后失败,通过提供真实的 handler 和状态来修复,不要为了让测试 setup 更少而把线上代码路径 props 再改回可选。
- 如果 story/test setup 显得冗长,创建满足严格线上代码路径契约的测试 helper 或 fixture;helper 保留在支持代码中,不要削弱组件 API。
- 对变更的 package 以及每个消费该变更组件的线上 app/package 运行 package 级 typecheck。
- 使用仓库特定的验证命令;在该 monorepo 中优先使用 `bun --bun run typecheck --filter <package>`。
- 当作为 CI agent 运行时,按照 `references/response-template.md` 格式化最终响应;该响应会成为 PR body。
- 最终响应中包含:收窄了多少个组件的摘要。
- 最终响应中包含:带有理由的变更表格。
- 最终响应中包含:证明每次收窄合理的线上调用点。
- 最终响应中包含:需要更新的支持代码(stories/tests)。
- 最终响应中包含:验证结果。
- 最终响应中包含:风险评估。
- 复查:变更后的 prop 类型是否从非测试、非 Storybook 调用点派生。
- 复查:是否对总是渲染的交互移除了可选回调。
- 复查:已渲染的 menu item 和 button 不会因缺少 handler 而成为惰性控件。
- 复查:被移除的 props 是否不被线上代码路径使用。
- 复查:是否只为真实状态保留 nullability,例如当前没有焦点。
- 复查:类型是否尽可能派生或提取,而不是手动复制。
- 复查:当推断会拓宽或掩盖目标状态时,是否使用 `useState<T>(...)`。
- 复查:共享变体是否针对同一套收窄后的契约编译通过。
- 复查:共享 package 和消费它的线上 app/package 是否通过 typecheck。
- 避免反模式:为了让 stories 能省略回调而把回调设为可选。
- 避免反模式:渲染调用 `onAction?.(...)` 的 menu item。
- 避免反模式:当线上代码是受控模式时,为 Storybook 添加 `default*` props。
- 避免反模式:用 `?? []` 或 `?? 0` 隐藏缺失的必需线上状态。
- 避免反模式:当线上代码只使用一种 API 形状时接受多种 API 形状。
- 避免反模式:把纯组件当作可放松契约的 mock 组件。
- 可参考示例 recurring GitHub Actions workflow:`references/agent-narrow-component-props.yml`;其示例 agent memory 文件是 `references/narrow-component-props-memory.md`;CI agent 响应格式见 `references/response-template.md`。
适用边界 · 注意事项
- 如果组件是面向外部使用者的通用库公共 API,需要刻意支持多种调用方或状态,不要仅按当前应用的线上调用点收窄。
- 如果没有可识别的非测试、非 Storybook 调用点,或组件只存在于 Storybook、测试或 demo 中,不要套用此技能。
- 如果类型放宽是产品/API 的有意设计,而不是为支持代码妥协,不要收窄。