Loading...
Loading...
Compare original and translation side by side
CreateUserFormUpdateUserFormCreateUserFormUpdateUserFormUserFormHeaderModalFooterWizard.StepProviderchildrenCreateUserFormUpdateUserFormChannelDialogThreadDialogstateactionsmetainputRefuseImperativeHandleUserFormHeaderModalFooterWizard.StepchildrenCreateUserFormUpdateUserFormChannelDialogThreadDialogstateactionsmetainputRefuseImperativeHandle<UserForm isUpdate><CreateUserForm><UpdateUserForm>UserFormProviderUserFormFieldsUserFormSubmit[{label, isSortable, divider, isMenu, items}]<Table.Column><Table.Divider><Table.MenuColumn><Dialog.Dropzone>disableDropzonerenderX{ state, actions, meta }actionsmetaactions={{ update: setState }}{ update, submit }useSyncedDraftuseFormStore<WizardProvider>{children}</WizardProvider>onStepChangeWizardProvider.tsxWizard.tsxmetauseImperativeHandleuseEffectuseEffect(() => onChange(state), [state])onFormStateDidChangeReact.FCitemsrenderItemchildrenvariant="ghost"components/<Button>{icon}{label}</Button><Button icon label /><UserForm isUpdate><CreateUserForm><UpdateUserForm>UserFormProviderUserFormFieldsUserFormSubmit[{label, isSortable, divider, isMenu, items}]<Table.Column><Table.Divider><Table.MenuColumn><Dialog.Dropzone>disableDropzonerenderX{ state, actions, meta }actionsmetaactions={{ update: setState }}{ update, submit }useSyncedDraftuseFormStore<WizardProvider>{children}</WizardProvider>onStepChangeWizardProvider.tsxWizard.tsxmetauseImperativeHandleuseEffectuseEffect(() => onChange(state), [state])onFormStateDidChangeReact.FCitemsrenderItemchildrenvariant="ghost"components/<Button>{icon}{label}</Button><Button icon label />CreateUserPageEditUserPageChannelComposerThreadComposerEditMessageComposerHeaderFieldsSubmitCreateUserPageEditUserPageChannelComposerThreadComposerEditMessageComposerHeaderFieldsSubmithasXdisableY[]setState(false)classNamehasXdisableY[]setState(false)className{ state, actions, meta }saving / error / success / handleSaveuseSectionForm(saveFn, onSaved){ saving, error, success, handleSave }useConfirm(){ confirm }await{ state, actions, meta }saving/error/success/handleSaveuseSectionForm(saveFn, onSaved){ saving, error, success, handleSave }useConfirm(){ confirm }awaitsubagent_type=Explore*Form*Modal*Dialog*Card*List*Wizard*Sheet*Drawer*Panel*ComposeruseEffectuseImperativeHandlePage → DayView → StaffColumn → AppointmentBlocksubagent_type=Explore*Form*Modal*Dialog*Card*List*Wizard*Sheet*Drawer*Panel*ComposeruseEffectuseImperativeHandlePage → DayView → StaffColumn → AppointmentBlockmode === "draft"mode === "published"mode === "draft"mode === "published"<UserFormProvider><UserFormFields><UserFormSubmit>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.
<UserFormProvider><UserFormFields><UserFormSubmit><UserForm.Fields>不要合并。如果一个范围较窄的发现(例如两个重复的员工表单)属于一个范围更广的发现(例如跨功能的生命周期模式),则将两者作为独立候选项列出。它们有不同的正确解决方案——范围较窄的可能需要提取Provider,而范围较广的可能需要Hook。不要因为大的发现“涵盖”小的发现就删除小的发现。100行的两个组件重复代码和10个调用方的生命周期Hook是独立的优化点。
metametauseEffectonFormStateDidChangerenderXisMenudivideritemsuseImperativeHandlemetacomponents/React.FCuseEffectonFormStateDidChangerenderXisMenudivideritemsuseImperativeHandlemetacomponents/React.FC// 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={...} />// 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>
);
}// 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={...} />// 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>
);
}// 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)} />
</>
);
}// 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>
);
}// 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)} />
</>
);
}// 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>
);
}// 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// 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 togets its own. The shared thing is the lifecycle shape, not the values. That's why a hook fits, not a provider.useSectionForm
// 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// 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之间没有共享状态——每次调用都会创建独立的状态。共享的是生命周期形状,而非值。这就是为什么使用Hook而非Provider的原因。useSectionForm
useConfirm()useConfirm()// 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)}
/>
</>
);
}// 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>
);
}// 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)}
/>
</>
);
}// 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>
);
}renderXrenderX// 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>// 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. That's the lifted-provider payoff.<ModalProvider>
// 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>// 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边界外(例如在页面粘性页脚中),仍然可以正常工作——只需将其渲染到内部即可。这就是状态提升Provider的价值。<ModalProvider>
// 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>// 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/hideXprops anywhere. The boolean is implicit in whether the JSX is present.disableX
// 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>// 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属性。布尔值隐含在JSX是否存在中。disableX
// 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" }] },
]}
/>// 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>// 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" }] },
]}
/>// 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><UserFormProvider>
<UserFormFields />
<UserFormSubmit />
</UserFormProvider><UserForm.Fields><UserFormProvider>
<UserFormFields />
<UserFormSubmit />
</UserFormProvider><UserForm.Fields>*.test.tsx*.spec.tsx*.stories.tsx*.gen.*node_modulesdistbuild.nextoutcomponents/ui/design-system/primitives/improve-react-code path/to/file.tsx*.test.tsx*.spec.tsx*.stories.tsx*.gen.*node_modulesdistbuild.nextoutcomponents/ui/design-system/primitives/improve-react-code path/to/file.tsx{ state, actions, meta }useState{ state, actions, meta }useState