improve-react-code

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Improve React Code

优化React代码

Find refactor opportunities in the React side of a codebase, focused on composition and lifted state.
React codebases drift toward monolithic components: every new variant adds another boolean prop, another render-prop slot, another optional field in a config array. Eventually the file is hard to peek inside — for humans and AI alike. The way out is composition — split variants into distinct components built from a small set of named subcomponents and a lifted state provider.
A 15-boolean monolith is worth fixing on its own, but the biggest wins come from clusters of similar siblings (e.g.
CreateUserForm
and
UpdateUserForm
) that could share the same extracted pieces. Lifting state and replacing prop bloat with JSX is where most friction relaxes.
在代码库的React部分寻找重构机会,重点关注组件组合与状态提升。
React代码库往往会逐渐演变为单体组件:每新增一个变体就多一个布尔属性、一个渲染属性插槽、一个配置数组中的可选字段。最终无论是开发者还是AI,都难以读懂这个文件。解决之道在于组件组合——将变体拆分为独立组件,这些组件由一小部分命名子组件和一个状态提升的Provider构建而成。
一个带有15个布尔属性的单体组件本身就值得重构,但最大的收益来自于相似的同类组件集群(例如
CreateUserForm
UpdateUserForm
),它们可以共享提取出的组件片段。通过提升状态并使用JSX替代冗余属性,能大幅降低代码维护的复杂度。

Glossary

术语表

  • Brick — a small, named subcomponent reused across variants (
    UserFormHeader
    ,
    ModalFooter
    ,
    Wizard.Step
    ).
  • Lifted provider — the context
    Provider
    extracted into its own component that takes
    children
    , so siblings outside the visual box can read its state and actions.
  • Sibling cluster — 2+ components in the same feature that share most of their structure but differ in variant logic (e.g.
    CreateUserForm
    and
    UpdateUserForm
    , or
    ChannelDialog
    and
    ThreadDialog
    ).
  • State slot
    state
    /
    actions
    /
    meta
    exposed via context. Children read from it; the implementation (useState, store, custom hook) lives in the component that renders the provider.
  • Meta — the slot for non-state values (e.g. an
    inputRef
    ) that several siblings need access to. Avoids
    useImperativeHandle
    and ref drilling.
  • Brick(基础组件) — 可在多个变体中复用的小型命名子组件(如
    UserFormHeader
    ModalFooter
    Wizard.Step
    )。
  • Lifted provider(状态提升Provider) — 提取为独立组件的Context Provider,接收
    children
    属性,使组件视觉边界外的兄弟组件也能读取其状态和操作方法。
  • Sibling cluster(同类组件集群) — 同一功能下的2个及以上组件,它们结构大部分相同,但变体逻辑不同(例如
    CreateUserForm
    UpdateUserForm
    ,或
    ChannelDialog
    ThreadDialog
    )。
  • State slot(状态插槽) — 通过Context暴露的
    state
    /
    actions
    /
    meta
    。子组件从中读取数据;具体实现(useState、状态库、自定义Hook)位于渲染Provider的组件中。
  • Meta(元数据插槽) — 用于存放多个兄弟组件需要访问的非状态值(例如
    inputRef
    )的插槽。避免使用
    useImperativeHandle
    和属性透传。

Core principles

核心原则

Audit findings should be framed in terms of these principles. Reference them by short name in the Problem field of each candidate.
  1. Boolean props that gate which subtree renders → split into distinct components.
    <UserForm isUpdate>
    becomes
    <CreateUserForm>
    and
    <UpdateUserForm>
    , sharing a
    UserFormProvider
    ,
    UserFormFields
    ,
    UserFormSubmit
    .
  2. Compound components. Build features as a Provider + named subcomponents (Header, Body, Footer, Submit, etc.) plus distinct action subcomponents.
  3. JSX over arrays-of-config for variant UI. Replace
    [{label, isSortable, divider, isMenu, items}]
    with
    <Table.Column>
    ,
    <Table.Divider>
    ,
    <Table.MenuColumn>
    .
  4. "Common" wrappers are JSX inside JSX*; they never take their own boolean variants.
  5. Don't render absent UI by toggling a boolean — just don't render it. A dialog that has no dropzone simply omits
    <Dialog.Dropzone>
    . No
    disableDropzone
    prop.
  6. Avoid
    renderX
    slot props as the primary surface.
    They don't survive when the slot needs to render outside the visual box (e.g. a Modal whose Forward button lives below the modal). Children-as-function — for sharing internal state with the caller — is a different, valid pattern.
  7. Provider exposes
    { state, actions, meta }
    .
    Implementation lives in the component that renders the provider. Children stay agnostic — they work whether state is ephemeral, persisted, or synced across devices. The
    actions
    and
    meta
    slots are open-ended bags — start small (
    actions={{ update: setState }}
    ) and grow them as new operations show up (
    { update, submit }
    ). When state comes from a custom hook (e.g.
    useSyncedDraft
    ,
    useFormStore
    ), the hook's return shape must conform to the provider's interface.
  8. Lift state by extracting the provider.
    <WizardProvider>{children}</WizardProvider>
    lets Next/Back buttons rendered as siblings of the wizard read context directly — no
    onStepChange
    callback or shared ref. In practice, this usually means moving the provider into its own file (
    WizardProvider.tsx
    ) so it can wrap the visual
    Wizard.tsx
    and whatever sits next to it.
  9. Use
    meta
    for refs and non-state values
    instead of
    useImperativeHandle
    or ref drilling.
  10. Don't sync sibling state via
    useEffect
    .
    A
    useEffect(() => onChange(state), [state])
    whose only job is lifting state up is the
    onFormStateDidChange
    smell. Lift the provider instead.
  11. Don't fear context re-renders. React Compiler memoizes consumers by the slice they actually read.
Plus, prop-API hygiene:
  • Don't use
    React.FC
    .
    Type props with an interface on the function parameter.
  • Prop API decision flow: lists →
    items
    +
    renderItem
    ; variants without internal data →
    children
    (compound); variants needing internal data → children-as-function; simple ReactNode swap → slot prop; 3+ props describing the same element → single slot prop; simple variants → value prop (
    variant="ghost"
    ).
  • File placement: feature folder first; shared
    components/
    only when 2+ features use it.
  • Composition over rigid props. Prefer
    <Button>{icon}{label}</Button>
    over
    <Button icon label />
    .
审计结果应基于以下原则展开。在每个候选重构项的问题字段中引用原则的简称。
  1. 用于控制子树渲染的布尔属性 → 拆分为独立组件。
    <UserForm isUpdate>
    可拆分为
    <CreateUserForm>
    <UpdateUserForm>
    ,二者共享
    UserFormProvider
    UserFormFields
    UserFormSubmit
  2. 复合组件模式。 以Provider + 命名子组件(Header、Body、Footer、Submit等)加上独立的操作子组件的方式构建功能。
  3. 用JSX替代配置数组实现变体UI。
    [{label, isSortable, divider, isMenu, items}]
    替换为
    <Table.Column>
    <Table.Divider>
    <Table.MenuColumn>
  4. “通用”包装组件是嵌套JSX;绝不使用自身布尔变体。
  5. 不要通过切换布尔值来隐藏UI——直接不渲染即可。 没有拖放区的对话框只需省略
    <Dialog.Dropzone>
    ,无需
    disableDropzone
    属性。
  6. 避免将
    renderX
    插槽属性作为主要交互方式。
    当插槽需要渲染到组件视觉边界外时(例如“下一步”按钮位于模态框下方的情况),这种方式无法生效。子组件作为函数——用于向调用方共享内部状态——是一种不同的有效模式。
  7. Provider暴露
    { state, actions, meta }
    具体实现位于渲染Provider的组件中。子组件保持无感知——无论状态是临时的、持久化的还是跨设备同步的,它们都能正常工作。
    actions
    meta
    是开放式的集合——从简单的
    actions={{ update: setState }}
    开始,随着新操作的出现逐步扩展为
    { update, submit }
    。当状态来自自定义Hook(例如
    useSyncedDraft
    useFormStore
    )时,Hook的返回结构必须符合Provider的接口规范。
  8. 通过提取Provider实现状态提升。
    <WizardProvider>{children}</WizardProvider>
    让向导组件的兄弟组件(上一步/下一步按钮)可以直接读取Context,无需
    onStepChange
    回调或共享Ref。实际操作中,这通常意味着将Provider移至独立文件(
    WizardProvider.tsx
    ),使其既能包裹可视化的
    Wizard.tsx
    ,也能包裹其旁边的其他组件。
  9. 使用
    meta
    存放Ref和非状态值
    ,替代
    useImperativeHandle
    或属性透传。
  10. 不要通过
    useEffect
    同步兄弟组件状态。
    仅用于向上提升状态的
    useEffect(() => onChange(state), [state])
    onFormStateDidChange
    式的不良代码。应改为提升Provider。
  11. 不必担心Context导致的重渲染。 React Compiler会根据组件实际读取的片段自动进行 memoize。
此外,属性API的规范:
  • 不要使用
    React.FC
    在函数参数上使用接口定义属性类型。
  • 属性API决策流程: 列表 →
    items
    +
    renderItem
    ;无内部数据的变体 →
    children
    (复合组件);需要内部数据的变体 → 子组件作为函数;简单ReactNode替换 → 插槽属性;3个及以上描述同一元素的属性 → 单个插槽属性;简单变体 → 值属性(
    variant="ghost"
    )。
  • 文件放置: 优先放在功能文件夹中;仅当2个及以上功能使用时,才放入共享的
    components/
    目录。
  • 组件组合优于固定属性。 优先使用
    <Button>{icon}{label}</Button>
    而非
    <Button icon label />

Threshold — when NOT to flag

阈值——无需标记的情况

Refuse to flag a component for compound-component refactoring unless at least one is true:
  • (a) ≥2 distinct callers shape it differently.
  • (b) The same condition is checked in ≥2 JSX branches inside one component.
  • (c) UI rendered outside the visual box needs the component's state/actions.
  • (d) Variants need different state-management implementations (ephemeral vs synced, etc.).
  • (e) Strongest signal — 2+ similar components reimplement the same internals instead of composing shared bricks. Distinct pages or variants are not the smell —
    CreateUserPage
    and
    EditUserPage
    existing as separate pages is correct, the same way Fernando's
    ChannelComposer
    ,
    ThreadComposer
    , and
    EditMessageComposer
    are correctly separate. The smell is when each one carries its own copy of the form fields, the upload handler, the lifecycle, instead of composing shared
    Header
    ,
    Fields
    ,
    Submit
    pieces. Look for the missing shared brick, not the existing sibling.
Below that threshold, single booleans, single render-prop slots, and children-as-function are fine. Don't refactor for the sake of it.
除非满足以下至少一项,否则不要标记组件需要进行复合组件重构:
  • (a) ≥2个不同的调用方以不同方式使用该组件。
  • (b) 同一条件在组件内部的≥2个JSX分支中被检查。
  • (c) 组件视觉边界外的UI需要访问该组件的状态/操作方法。
  • (d) 变体需要不同的状态管理实现(临时状态 vs 同步状态等)。
  • (e) 最强烈信号 — 2个及以上相似组件重复实现相同的内部逻辑,而非组合共享的基础组件。独立页面或变体本身不是问题——
    CreateUserPage
    EditUserPage
    作为独立页面是合理的,就像Fernando的
    ChannelComposer
    ThreadComposer
    EditMessageComposer
    各自独立是合理的一样。问题在于每个组件都携带自己的表单字段、上传处理逻辑、生命周期代码,而非组合共享的
    Header
    Fields
    Submit
    组件片段。要寻找的是缺失的共享基础组件,而非已存在的同类组件。
低于该阈值时,单个布尔属性、单个渲染属性插槽、子组件作为函数的写法都是可以接受的。不要为了重构而重构。

When duplication is the right call

重复代码是合理选择的场景

DRY is a goal, not a rule. Prefer duplication over the wrong abstraction. Wrong abstractions accumulate conditionals over time as new requirements force them to flex; duplication stays honest and easy to delete. Leave repetition alone when:
  • Two callers + small block. Rule of Three: asking "haven't I written this before?" twice is fine — on the third occurrence the pattern starts to scream. Two callers of a 5-line block is below threshold. Two callers of a 100-line form is not — substantial duplication earns extraction at two.
  • The duplicates might evolve independently. Two forms in different domains (staff vs client, payments vs shipping) may look identical today and diverge as each domain grows. Forcing them through one shared API makes future divergence painful — the abstraction grows conditional flags to accommodate. Optimize for change.
  • The abstraction would leak its implementation. A hook with 8 parameters, a brick with 5 render-prop slots, or a component with
    hasX
    /
    disableY
    flags is the abstraction failing. Two duplicated implementations are more readable than one over-parameterized one.
  • The repeated block is trivial. A default
    []
    , a
    setState(false)
    reset, a
    className
    concatenation — these don't earn names. Reading them in place beats jumping to another file.
  • The use cases haven't crystallized. If you'd have to guess what callers will need, you'll guess wrong. Wait for the patterns to scream — not whisper.
When in doubt, leave the duplication. The audit should propose abstractions only when the patterns scream.
DRY(Don't Repeat Yourself)是目标而非规则。与其使用错误的抽象,不如保留重复代码。错误的抽象会随着新需求的出现积累大量条件判断;而重复代码则保持清晰,易于删除。在以下场景中保留重复代码:
  • 两个调用方 + 小块代码。 三次原则:第一次、第二次觉得“我是不是写过这个?”是正常的——第三次出现时,模式才真正清晰。两个调用方使用5行代码块的情况低于阈值;但两个调用方使用100行表单代码的情况则不同——大量重复代码在出现两次时就值得提取。
  • 重复代码可能会独立演化。 不同领域的两个表单(员工端 vs 客户端,支付 vs 物流)现在看起来完全相同,但随着各自领域的发展可能会逐渐分化。强制它们使用同一个共享API会导致未来的分化变得痛苦——抽象会积累条件标志来适应变化。要为变化做好优化。
  • 抽象会暴露其实现细节。 带有8个参数的Hook、带有5个渲染属性插槽的基础组件,或带有
    hasX
    /
    disableY
    标志的组件,都是抽象失败的表现。两个重复实现比一个过度参数化的抽象更易读。
  • 重复代码块很简单。 默认值
    []
    setState(false)
    重置、
    className
    拼接——这些不需要命名。直接在原地阅读比跳转到另一个文件更好。
  • 使用场景尚未明确。 如果需要猜测调用方的需求,很可能会猜错。等待模式清晰显现——而非隐约出现。
如有疑问,保留重复代码。只有当模式清晰显现时,审计才应提出抽象方案。

Shape — provider vs custom hook

抽象形式——Provider vs 自定义Hook

Once a finding passes the threshold, decide what shape the abstraction takes. Don't reach for a provider every time.
  • Shared state across siblingslifted provider with
    { state, actions, meta }
    . Use this when multiple components need to read or update the same values (wizard step, selected appointment, draft form contents).
  • Shared lifecycle around a callbackcustom hook. Use this when each consumer owns its own state but repeats the same
    saving / error / success / handleSave
    boilerplate around a callback.
    useSectionForm(saveFn, onSaved)
    returning
    { saving, error, success, handleSave }
    is the canonical shape.
  • One-shot imperative interactions (confirm dialogs, toasts) → imperative hook like
    useConfirm()
    returning
    { confirm }
    that owns its own open/saving lifecycle internally. The call site just
    await
    s it.
If the proposed solution is a hook, none of the compound-component principles (provider, frame, named subcomponents) apply — the hook is the abstraction.
当某个候选重构项通过阈值检查后,决定抽象的形式。不要每次都选择Provider。
  • 兄弟组件间共享状态状态提升Provider,包含
    { state, actions, meta }
    。当多个组件需要读取或更新相同值(向导步骤、选中的预约、草稿表单内容)时使用这种方式。
  • 围绕回调的共享生命周期自定义Hook。当每个消费者拥有自己的状态,但重复实现相同的
    saving/error/success/handleSave
    回调模板时使用这种方式。
    useSectionForm(saveFn, onSaved)
    返回
    { saving, error, success, handleSave }
    是典型的形式。
  • 一次性命令式交互(确认对话框、提示框)→ 命令式Hook,如
    useConfirm()
    返回
    { confirm }
    ,内部管理自己的打开/保存生命周期。调用方只需
    await
    即可。
如果解决方案是Hook,则复合组件的原则(Provider、框架、命名子组件)均不适用——Hook本身就是抽象。

Process

流程

1. Explore

1. 探索

Use the Agent tool with
subagent_type=Explore
to walk the React side of the codebase. Don't follow rigid heuristics — explore organically, but also:
  • Scan for name-stem clusters (
    *Form
    ,
    *Modal
    ,
    *Dialog
    ,
    *Card
    ,
    *List
    ,
    *Wizard
    ,
    *Sheet
    ,
    *Drawer
    ,
    *Panel
    ,
    *Composer
    ).
  • For each cluster, read the candidate components and judge structural similarity by eye. Two components share a "substructure" when they render a recognizable named region (header, body, footer, action row, input area, etc.) the same way. A cluster qualifies when ≥2 components share ≥3 substructures.
  • Note components with high prop count (many booleans),
    useEffect
    whose only job is calling a parent setter,
    useImperativeHandle
    outside design-system primitives, and array-of-config UI with heterogeneous item shapes.
  • Trace prop-drilling depth. Pick stateful props (state values, setters, callbacks owned higher up). Follow each one down the tree. If a prop is forwarded through ≥3 components that do not consume it themselves, that's threshold-(c) firing — the leaf component is reaching back to the root via the intermediate layers. "It works" is not an excuse: pass-through layers are exactly the smell. Cite the chain explicitly in the finding (e.g.
    Page → DayView → StaffColumn → AppointmentBlock
    ).
Apply the threshold before promoting any finding into the report.
使用
subagent_type=Explore
的Agent工具遍历代码库的React部分。不要遵循僵化的规则——有机地探索,但同时:
  • 扫描名称前缀集群
    *Form
    *Modal
    *Dialog
    *Card
    *List
    *Wizard
    *Sheet
    *Drawer
    *Panel
    *Composer
    )。
  • 对于每个集群,阅读候选组件并通过肉眼判断结构相似性。当两个组件以相同方式渲染可识别的命名区域(页眉、主体页脚、操作栏、输入区域等)时,它们共享“子结构”。当≥2个组件共享≥3个子结构时,该集群符合条件。
  • 记录具有大量属性(多个布尔值)的组件、仅用于调用父组件setter的
    useEffect
    、在设计系统基础组件外使用的
    useImperativeHandle
    ,以及包含异构项形状的配置数组UI。
  • 追踪属性透传深度。选择有状态的属性(状态值、setter、上层组件定义的回调)。追踪每个属性在组件树中的传递路径。如果一个属性被透传过≥3个不直接使用它的组件,就触发了阈值(c)——叶子组件通过中间层回溯到根组件。“能运行”不是借口:透传层正是问题所在。在审计结果中明确引用该链(例如
    Page → DayView → StaffColumn → AppointmentBlock
    )。
在将任何发现纳入报告前,先应用阈值检查。

2. Verify before promoting

2. 验证后再纳入报告

For each candidate that passes the threshold, run three checks before adding it to the report. This filters out the most common failure modes: hallucinated extractions, premature abstractions, and misidentified state machines.
  1. Structural identity check. Are the alleged duplicates actually structurally identical, or do they just look similar? Open both files. List the substructures (header, body, footer, validation, error handling). Compare 1:1. If the same JSX shape is rendered with different validators, different success paths, or different button arrangements, the duplication is shallower than it looks.
  2. Load-bearing divergence check. Would the proposed abstraction force divergent cases through one API surface? If a brick proposed for "shared chrome" would force some consumers to use escape hatches because their footer, header, or action shape genuinely differs (one needs Save/Cancel, another needs Activate/Deactivate, another needs no buttons), the brick is too thin. Propose a richer API (slot, children, render-prop) — or downgrade the finding to "split the file" without proposing a chrome brick.
  3. State-machine vs boolean-bloat check. A boolean or enum prop deciding which subtree renders is a smell only when the branches are variants of the same thing. If
    mode === "draft"
    shows an Edit button and
    mode === "published"
    shows an Unpublish button, those are semantically distinct operations on different states — that's a state machine, and the branching is correct. Don't flag.
If any check fails, demote or drop. Demote Critical → Medium → Bikeshedding when the underlying signal is real but the proposed shape is wrong. Drop entirely when the finding is a hallucination (the alleged duplicate doesn't exist) or a misidentified state machine. Note dropped findings briefly in the "What I left out" section so the user knows they were considered.
对于每个通过阈值检查的候选项,在添加到报告前运行三项检查。这可以过滤掉最常见的错误模式:虚构的提取、过早的抽象、错误识别的状态机。
  1. 结构一致性检查。所谓的重复代码是否真的结构一致,还是只是看起来相似?打开两个文件。列出子结构(页眉、主体、页脚、验证、错误处理)。逐一对比。如果渲染相同的JSX形状,但使用不同的验证器、不同的成功路径或不同的按钮布局,则重复程度比看起来更浅。
  2. 关键差异检查。提议的抽象是否会迫使不同的场景使用同一个API表面?如果为“共享框架”提议的基础组件会迫使某些消费者使用逃逸舱口,因为它们的页脚、页眉或操作形状确实不同(一个需要保存/取消,另一个需要激活/停用,还有一个不需要按钮),则该基础组件的设计过于单薄。提议更丰富的API(插槽、children、渲染属性)——或者将审计结果降级为“拆分文件”,不提议框架基础组件。
  3. 状态机 vs 布尔属性冗余检查。用于决定渲染哪个子树的布尔或枚举属性只有在分支是同一事物的变体时才是问题。如果
    mode === "draft"
    显示编辑按钮,
    mode === "published"
    显示取消发布按钮,这些是针对不同状态的语义不同的操作——这是状态机,分支是合理的。不要标记。
如果任何一项检查失败,降级或删除该候选项。当底层信号真实但提议的形式错误时,将严重程度从关键→中等→细枝末节降级。当发现是虚构的(所谓的重复代码不存在)或错误识别的状态机时,完全删除该候选项。在“未纳入的内容”部分简要说明删除的候选项,让用户知道这些内容已被考虑过。

3. Present numbered candidates

3. 列出编号的候选项

Return a numbered list. Each candidate:
  • Files — paths involved.
  • Problem — which principle is being violated, in plain English. Reference the principle by short name.
  • Solution — sketch the proposed compound API in flat-export style (
    <UserFormProvider>
    ,
    <UserFormFields>
    ,
    <UserFormSubmit>
    ) — unless the codebase already uses dot-namespaced compound, in which case match that.
  • Reuse map — which existing components would consume the extracted bricks. Cite real consumers only — don't speculate about hypothetical future use cases ("future bulk import," "could be used for X"). This is what makes a finding Critical.
  • Severity — Critical / Medium / Bikeshedding (see below).
  • Benefits — testability, fewer call-site shapes, AI navigability.
Do NOT propose detailed interfaces yet. Ask: "Which of these would you like to explore?"
No subsumption. If a narrow finding (e.g. two duplicated staff forms) fits inside a broader one (e.g. a cross-feature lifecycle pattern), surface both as separate candidates. They have different right answers — the narrow one may need a provider extraction while the broader one needs a hook. Don't drop the smaller finding because the bigger one "covers it." A 100-line two-component duplication and a 10-call-site lifecycle hook are independent wins.
返回编号列表。每个候选项包括:
  • 文件 — 涉及的路径。
  • 问题 — 用通俗易懂的语言说明违反了哪条原则。引用原则的简称。
  • 解决方案 — 以扁平导出风格勾勒提议的复合API(
    <UserFormProvider>
    <UserFormFields>
    <UserFormSubmit>
    )——除非代码库已经使用点命名空间的复合组件(
    <UserForm.Fields>
    ),此时应遵循现有约定。
  • 复用映射 — 哪些现有组件会使用提取出的基础组件。仅引用真实的消费者——不要推测未来的假设使用场景(“未来的批量导入”“可用于X”)。这是将发现标记为关键的依据。
  • 严重程度 — 关键/中等/细枝末节(见下文)。
  • 收益 — 可测试性、更少的调用方形式、AI可导航性。
暂不提议详细的接口。询问:“您想深入探索哪一个?”
不要合并。如果一个范围较窄的发现(例如两个重复的员工表单)属于一个范围更广的发现(例如跨功能的生命周期模式),则将两者作为独立候选项列出。它们有不同的正确解决方案——范围较窄的可能需要提取Provider,而范围较广的可能需要Hook。不要因为大的发现“涵盖”小的发现就删除小的发现。100行的两个组件重复代码和10个调用方的生命周期Hook是独立的优化点。

4. Grilling loop

4. 深入设计循环

Once the user picks a candidate, walk the design tree: what becomes a brick, what stays a one-off, where the provider sits, who lifts state, what
meta
carries, which siblings need access. Output a recommended interface (set of compound parts + provider shape) that the user can hand to Claude to implement.
一旦用户选择了一个候选项,逐步梳理设计:哪些部分成为基础组件,哪些部分保留为一次性组件,Provider的位置,谁负责提升状态,
meta
携带什么数据,哪些兄弟组件需要访问。输出推荐的接口(复合组件集合 + Provider形状),用户可以直接交给Claude实现。

Detection signals, tiered

检测信号,分级

Critical — lead the report:
  • Sibling clusters with shared bricks (threshold clause e).
  • ≥3 boolean props gating JSX subtrees, used in ≥2 distinct shapes.
  • State sync between siblings via
    useEffect
    (
    onFormStateDidChange
    smell).
  • UI outside the visual box reaching back in via callbacks or shared refs.
Medium:
  • Same condition checked in ≥2 JSX branches inside one component.
  • renderX
    slot prop where the slot needs internal state and could live in context.
  • Array-of-config UI with heterogeneous item shapes (
    isMenu
    ,
    divider
    ,
    items
    ).
  • useImperativeHandle
    for things that could live in context
    meta
    .
  • File placement (feature-folder vs shared
    components/
    ).
Bikeshedding — collapsed at the bottom of the report:
  • React.FC
    usage.
  • Lone components with 1–2 boolean props and a single call site.
  • Render-prop slots that work fine and don't escape the box.
  • 3+ props describing the same element with only 1 caller.
关键 — 放在报告开头:
  • 共享基础组件的同类组件集群(阈值条款e)。
  • ≥3个控制JSX子树渲染的布尔属性,且以≥2种不同形式使用。
  • 通过
    useEffect
    同步兄弟组件状态(
    onFormStateDidChange
    式不良代码)。
  • 组件视觉边界外的UI通过回调或共享Ref回溯到组件内部。
中等
  • 同一条件在组件内部的≥2个JSX分支中被检查。
  • renderX
    插槽属性需要访问内部状态,且可通过Context实现。
  • 包含异构项形状的配置数组UI(
    isMenu
    divider
    items
    )。
  • 使用
    useImperativeHandle
    处理可放入Context
    meta
    的内容。
  • 文件放置位置(功能文件夹 vs 共享
    components/
    )。
细枝末节 — 折叠在报告底部:
  • 使用
    React.FC
  • 只有1-2个布尔属性且只有一个调用方的独立组件。
  • 运行正常且无需突破边界的渲染属性插槽。
  • 3个及以上描述同一元素的属性,但只有一个调用方。

Examples

示例

Five canonical refactors. Use these as templates when sketching Solution shapes in the report.
五个典型的重构案例。在报告中勾勒解决方案形状时,可将这些作为模板。

1. Boolean-bloat → split into siblings sharing bricks

1. 布尔属性冗余 → 拆分为共享基础组件的同类组件

tsx
// BEFORE
function UserForm({ isUpdate, hideWelcome, hideTerms, onSuccess }) {
  const user = isUpdate ? useUser() : null;
  return (
    <form>
      {!hideWelcome && <Welcome />}
      <Fields initialUser={user} />
      {!hideTerms && <Terms />}
      <button>{isUpdate ? "Save" : "Create"}</button>
    </form>
  );
}

// usage:
<UserForm isUpdate hideWelcome hideTerms onSuccess={...} />
<UserForm onSuccess={...} />
tsx
// AFTER
function UserFormProvider({ initialUser, children }) {
  const [draft, setDraft] = useState(initialUser ?? emptyUser);
  return <Ctx.Provider value={{ draft, setDraft }}>{children}</Ctx.Provider>;
}
function UserFormFields() { /* reads ctx */ }
function UserFormSubmit({ children }) { /* reads ctx, posts */ }

function CreateUserForm() {
  return (
    <UserFormProvider>
      <Welcome />
      <UserFormFields />
      <Terms />
      <UserFormSubmit>Create</UserFormSubmit>
    </UserFormProvider>
  );
}

function UpdateUserForm() {
  const user = useUser();
  return (
    <UserFormProvider initialUser={user}>
      <UserFormFields />
      <UserFormSubmit>Save</UserFormSubmit>
    </UserFormProvider>
  );
}
tsx
// BEFORE
function UserForm({ isUpdate, hideWelcome, hideTerms, onSuccess }) {
  const user = isUpdate ? useUser() : null;
  return (
    <form>
      {!hideWelcome && <Welcome />}
      <Fields initialUser={user} />
      {!hideTerms && <Terms />}
      <button>{isUpdate ? "Save" : "Create"}</button>
    </form>
  );
}

// usage:
<UserForm isUpdate hideWelcome hideTerms onSuccess={...} />
<UserForm onSuccess={...} />
tsx
// AFTER
function UserFormProvider({ initialUser, children }) {
  const [draft, setDraft] = useState(initialUser ?? emptyUser);
  return <Ctx.Provider value={{ draft, setDraft }}>{children}</Ctx.Provider>;
}
function UserFormFields() { /* reads ctx */ }
function UserFormSubmit({ children }) { /* reads ctx, posts */ }

function CreateUserForm() {
  return (
    <UserFormProvider>
      <Welcome />
      <UserFormFields />
      <Terms />
      <UserFormSubmit>Create</UserFormSubmit>
    </UserFormProvider>
  );
}

function UpdateUserForm() {
  const user = useUser();
  return (
    <UserFormProvider initialUser={user}>
      <UserFormFields />
      <UserFormSubmit>Save</UserFormSubmit>
    </UserFormProvider>
  );
}

2. Sibling state-sync via callback → lifted provider

2. 通过回调同步兄弟组件状态 → 状态提升Provider

tsx
// BEFORE — Next/Back live outside the wizard, state is drilled
function Page() {
  const [step, setStep] = useState(0);
  return (
    <>
      <Wizard step={step} onStepChange={setStep} />
      <BackButton onClick={() => setStep((s) => s - 1)} />
      <NextButton onClick={() => setStep((s) => s + 1)} />
    </>
  );
}
tsx
// AFTER — provider wraps Wizard *and* the buttons
function WizardProvider({ children }) {
  const [step, setStep] = useState(0);
  return <Ctx.Provider value={{ step, setStep }}>{children}</Ctx.Provider>;
}
function Wizard() { const { step } = useWizard(); /* renders step UI */ }
function BackButton() { const { setStep } = useWizard(); /* ... */ }
function NextButton() { const { setStep } = useWizard(); /* ... */ }

function Page() {
  return (
    <WizardProvider>
      <Wizard />
      <BackButton />
      <NextButton />
    </WizardProvider>
  );
}
tsx
// BEFORE — Next/Back live outside the wizard, state is drilled
function Page() {
  const [step, setStep] = useState(0);
  return (
    <>
      <Wizard step={step} onStepChange={setStep} />
      <BackButton onClick={() => setStep((s) => s - 1)} />
      <NextButton onClick={() => setStep((s) => s + 1)} />
    </>
  );
}
tsx
// AFTER — provider wraps Wizard *and* the buttons
function WizardProvider({ children }) {
  const [step, setStep] = useState(0);
  return <Ctx.Provider value={{ step, setStep }}>{children}</Ctx.Provider>;
}
function Wizard() { const { step } = useWizard(); /* renders step UI */ }
function BackButton() { const { setStep } = useWizard(); /* ... */ }
function NextButton() { const { setStep } = useWizard(); /* ... */ }

function Page() {
  return (
    <WizardProvider>
      <Wizard />
      <BackButton />
      <NextButton />
    </WizardProvider>
  );
}

3. Repeated lifecycle → custom hook (not a provider)

3. 重复生命周期 → 自定义Hook(非Provider)

tsx
// BEFORE — every section reimplements the same save lifecycle
function ProfileSection() {
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);
  const handleSave = async () => {
    setSaving(true); setError(null);
    try { await saveProfile(); setSuccess(true); }
    catch (e) { setError(e); }
    finally { setSaving(false); }
  };
  return /* ... */;
}
// PreferencesSection, BookingSection — same 12 lines copy-pasted
tsx
// AFTER — one hook, three call sites, each owns its own state
function useSectionForm(saveFn) {
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);
  const handleSave = async () => {
    setSaving(true); setError(null);
    try { await saveFn(); setSuccess(true); }
    catch (e) { setError(e); }
    finally { setSaving(false); }
  };
  return { saving, error, success, handleSave };
}

function ProfileSection() {
  const { saving, error, handleSave } = useSectionForm(saveProfile);
  return /* ... */;
}
Note: there is no shared state across sections — each call to
useSectionForm
gets its own. The shared thing is the lifecycle shape, not the values. That's why a hook fits, not a provider.
tsx
// BEFORE — every section reimplements the same save lifecycle
function ProfileSection() {
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);
  const handleSave = async () => {
    setSaving(true); setError(null);
    try { await saveProfile(); setSuccess(true); }
    catch (e) { setError(e); }
    finally { setSaving(false); }
  };
  return /* ... */;
}
// PreferencesSection, BookingSection — same 12 lines copy-pasted
tsx
// AFTER — one hook, three call sites, each owns its own state
function useSectionForm(saveFn) {
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);
  const handleSave = async () => {
    setSaving(true); setError(null);
    try { await saveFn(); setSuccess(true); }
    catch (e) { setError(e); }
    finally { setSaving(false); }
  };
  return { saving, error, success, handleSave };
}

function ProfileSection() {
  const { saving, error, handleSave } = useSectionForm(saveProfile);
  return /* ... */;
}
注意:各section之间没有共享状态——每次调用
useSectionForm
都会创建独立的状态。共享的是生命周期形状,而非值。这就是为什么使用Hook而非Provider的原因。

4. Per-caller confirm state → imperative
useConfirm()
hook

4. 调用方独立的确认状态 → 命令式
useConfirm()
Hook

tsx
// BEFORE — every consumer manages its own confirm dialog
function AppointmentCard() {
  const [pendingDelete, setPendingDelete] = useState(false);
  const [deleting, setDeleting] = useState(false);
  return (
    <>
      <button onClick={() => setPendingDelete(true)}>Delete</button>
      <ConfirmDialog
        open={pendingDelete}
        saving={deleting}
        onConfirm={async () => { setDeleting(true); await deleteAppt(); }}
        onCancel={() => setPendingDelete(false)}
      />
    </>
  );
}
tsx
// AFTER — call site is one line; the hook owns the dialog
function AppointmentCard() {
  const { confirm } = useConfirm();
  return (
    <button
      onClick={async () => {
        if (await confirm({ title: "Delete?", message: "Cannot be undone." })) {
          await deleteAppt();
        }
      }}
    >
      Delete
    </button>
  );
}
tsx
// BEFORE — every consumer manages its own confirm dialog
function AppointmentCard() {
  const [pendingDelete, setPendingDelete] = useState(false);
  const [deleting, setDeleting] = useState(false);
  return (
    <>
      <button onClick={() => setPendingDelete(true)}>Delete</button>
      <ConfirmDialog
        open={pendingDelete}
        saving={deleting}
        onConfirm={async () => { setDeleting(true); await deleteAppt(); }}
        onCancel={() => setPendingDelete(false)}
      />
    </>
  );
}
tsx
// AFTER — call site is one line; the hook owns the dialog
function AppointmentCard() {
  const { confirm } = useConfirm();
  return (
    <button
      onClick={async () => {
        if (await confirm({ title: "Delete?", message: "Cannot be undone." })) {
          await deleteAppt();
        }
      }}
    >
      Delete
    </button>
  );
}

5.
renderX
slot prop → compound subcomponent reading context

5.
renderX
插槽属性 → 读取Context的复合子组件

tsx
// BEFORE — footer slot can't access modal's internal state
function Modal({ title, renderFooter, children }) {
  const [busy, setBusy] = useState(false);
  return (
    <Dialog>
      <header>{title}</header>
      <main>{children}</main>
      <footer>{renderFooter?.({ busy, setBusy })}</footer>
    </Dialog>
  );
}

<Modal
  title="Edit"
  renderFooter={({ busy, setBusy }) => (
    <button disabled={busy} onClick={() => { setBusy(true); save(); }}>Save</button>
  )}
>
  <Form />
</Modal>
tsx
// AFTER — Modal.Footer is a real component reading modal context
function ModalProvider({ children }) {
  const [busy, setBusy] = useState(false);
  return <Ctx.Provider value={{ busy, setBusy }}>{children}</Ctx.Provider>;
}
function Modal({ children }) { /* renders Dialog shell */ }
function ModalHeader({ children }) { /* ... */ }
function ModalBody({ children }) { /* ... */ }
function ModalFooter({ children }) { /* ... */ }

<ModalProvider>
  <Modal>
    <ModalHeader>Edit</ModalHeader>
    <ModalBody><Form /></ModalBody>
    <ModalFooter>
      <SaveButton />  {/* reads { busy, setBusy } from context */}
    </ModalFooter>
  </Modal>
</ModalProvider>
If the Save button needs to live outside the Modal box (e.g. in a sticky page footer), it still works — it just needs to be rendered inside
<ModalProvider>
. That's the lifted-provider payoff.
tsx
// BEFORE — footer slot can't access modal's internal state
function Modal({ title, renderFooter, children }) {
  const [busy, setBusy] = useState(false);
  return (
    <Dialog>
      <header>{title}</header>
      <main>{children}</main>
      <footer>{renderFooter?.({ busy, setBusy })}</footer>
    </Dialog>
  );
}

<Modal
  title="Edit"
  renderFooter={({ busy, setBusy }) => (
    <button disabled={busy} onClick={() => { setBusy(true); save(); }}>Save</button>
  )}
>
  <Form />
</Modal>
tsx
// AFTER — Modal.Footer is a real component reading modal context
function ModalProvider({ children }) {
  const [busy, setBusy] = useState(false);
  return <Ctx.Provider value={{ busy, setBusy }}>{children}</Ctx.Provider>;
}
function Modal({ children }) { /* renders Dialog shell */ }
function ModalHeader({ children }) { /* ... */ }
function ModalBody({ children }) { /* ... */ }
function ModalFooter({ children }) { /* ... */ }

<ModalProvider>
  <Modal>
    <ModalHeader>Edit</ModalHeader>
    <ModalBody><Form /></ModalBody>
    <ModalFooter>
      <SaveButton />  {/* reads { busy, setBusy } from context */}
    </ModalFooter>
  </Modal>
</ModalProvider>
如果保存按钮需要渲染到Modal边界外(例如在页面粘性页脚中),仍然可以正常工作——只需将其渲染到
<ModalProvider>
内部即可。这就是状态提升Provider的价值。

6. Hide-flags → just don't render the part

6. 隐藏标志 → 直接不渲染对应部分

tsx
// BEFORE — a single component with toggles for every variant
function Dialog({ title, hideHeader, hideTitle, disableDropzone, children }) {
  return (
    <Shell>
      {!hideHeader && (
        <header>{!hideTitle && <h2>{title}</h2>}</header>
      )}
      {!disableDropzone && <Dropzone />}
      <main>{children}</main>
    </Shell>
  );
}

<Dialog title="Edit" hideHeader disableDropzone>...</Dialog>
<Dialog title="Forward" hideTitle>...</Dialog>
tsx
// AFTER — the absence of the part *is* the variant
function Dialog({ children }) { return <Shell>{children}</Shell>; }
function DialogHeader({ children }) { return <header>{children}</header>; }
function DialogTitle({ children }) { return <h2>{children}</h2>; }
function DialogDropzone() { /* ... */ }

// Edit dialog has no header and no dropzone — just don't render them
<Dialog>
  <main>...</main>
</Dialog>

// Forward dialog has a header but no title
<Dialog>
  <DialogHeader>...</DialogHeader>
  <DialogDropzone />
  <main>...</main>
</Dialog>
No
hideX
/
disableX
props anywhere. The boolean is implicit in whether the JSX is present.
tsx
// BEFORE — a single component with toggles for every variant
function Dialog({ title, hideHeader, hideTitle, disableDropzone, children }) {
  return (
    <Shell>
      {!hideHeader && (
        <header>{!hideTitle && <h2>{title}</h2>}</header>
      )}
      {!disableDropzone && <Dropzone />}
      <main>{children}</main>
    </Shell>
  );
}

<Dialog title="Edit" hideHeader disableDropzone>...</Dialog>
<Dialog title="Forward" hideTitle>...</Dialog>
tsx
// AFTER — the absence of the part *is* the variant
function Dialog({ children }) { return <Shell>{children}</Shell>; }
function DialogHeader({ children }) { return <header>{children}</header>; }
function DialogTitle({ children }) { return <h2>{children}</h2>; }
function DialogDropzone() { /* ... */ }

// Edit dialog has no header and no dropzone — just don't render them
<Dialog>
  <main>...</main>
</Dialog>

// Forward dialog has a header but no title
<Dialog>
  <DialogHeader>...</DialogHeader>
  <DialogDropzone />
  <main>...</main>
</Dialog>
不再有任何
hideX
/
disableX
属性。布尔值隐含在JSX是否存在中。

7. Array-of-config → JSX subcomponents

7. 配置数组 → JSX子组件

tsx
// BEFORE — heterogeneous item shapes hidden behind a config array
<DataTable
  columns={[
    { key: "name", label: "Name", isSortable: true },
    { divider: true },
    { isMenu: true, items: [{ label: "Edit" }, { label: "Delete" }] },
  ]}
/>
tsx
// AFTER — flat JSX, easy to escape into one-offs
<DataTable>
  <DataTable.Column field="name" sortable>Name</DataTable.Column>
  <DataTable.Divider />
  <DataTable.MenuColumn>
    <DataTable.MenuItem>Edit</DataTable.MenuItem>
    <DataTable.MenuItem>Delete</DataTable.MenuItem>
  </DataTable.MenuColumn>
</DataTable>
tsx
// BEFORE — heterogeneous item shapes hidden behind a config array
<DataTable
  columns={[
    { key: "name", label: "Name", isSortable: true },
    { divider: true },
    { isMenu: true, items: [{ label: "Edit" }, { label: "Delete" }] },
  ]}
/>
tsx
// AFTER — flat JSX, easy to escape into one-offs
<DataTable>
  <DataTable.Column field="name" sortable>Name</DataTable.Column>
  <DataTable.Divider />
  <DataTable.MenuColumn>
    <DataTable.MenuItem>Edit</DataTable.MenuItem>
    <DataTable.MenuItem>Delete</DataTable.MenuItem>
  </DataTable.MenuColumn>
</DataTable>

Style for proposed solutions

解决方案的风格

Default to flat separate exports:
tsx
<UserFormProvider>
  <UserFormFields />
  <UserFormSubmit />
</UserFormProvider>
If the codebase already uses dot-namespaced compound (
<UserForm.Fields>
), match that — respect existing convention. The underlying composition principles are identical either way.
默认使用扁平独立导出
tsx
<UserFormProvider>
  <UserFormFields />
  <UserFormSubmit />
</UserFormProvider>
如果代码库已经使用点命名空间的复合组件(
<UserForm.Fields>
),则遵循现有约定——尊重已有的代码规范。底层的组件组合原则是相同的。

Skips (default)

默认跳过的文件

  • *.test.tsx
    ,
    *.spec.tsx
    ,
    *.stories.tsx
    .
  • Generated/codegen output (
    *.gen.*
    , GraphQL types).
  • node_modules
    ,
    dist
    ,
    build
    ,
    .next
    ,
    out
    .
  • Design-system primitives by location (
    components/ui/
    ,
    design-system/
    ,
    primitives/
    ) and tiny single-export leaf files.
  • Files under 50 lines.
User-targeted paths (e.g.
improve-react-code path/to/file.tsx
) bypass every skip — explicit beats implicit.
  • *.test.tsx
    *.spec.tsx
    *.stories.tsx
  • 生成的代码(
    *.gen.*
    、GraphQL类型)。
  • node_modules
    dist
    build
    .next
    out
    目录。
  • 位于指定目录的设计系统基础组件(
    components/ui/
    design-system/
    primitives/
    ),以及小型单导出叶子文件。
  • 少于50行的文件。
用户指定的路径(例如
improve-react-code path/to/file.tsx
)会跳过所有默认规则——显式指令优先于隐式规则。

Notes

说明

  • Agnostic to state-management library. The provider's interface is
    { state, actions, meta }
    ; the implementation can be
    useState
    , Zustand, Redux, a sync hook, or anything else.
  • React Compiler removes most context-perf objections — a consumer is memoized by the slice it reads.
  • 与状态管理库无关。Provider的接口是
    { state, actions, meta }
    ;具体实现可以是
    useState
    、Zustand、Redux、同步Hook或其他任何方式。
  • React Compiler消除了大多数关于Context性能的顾虑——组件会根据实际读取的片段自动进行memoize。