・Too abstract props lead to repetitive boilerplate implementations in component callers, resulting in increased code duplication.
Bad example
・This Dialog requires us to create actions prop such as OK/Cancel, receiving an array as actions and a function as onAction.
type Action = { label: string; type: string };
type DialogProps = {
title: string;
isOpen: boolean;
onClose: () => void;
actions: Action[]; // Everything can put.
onAction: (type: string) => void; //Everything can be return.
};
export const AnyDialog: React.FC<React.PropsWithChildren<DialogProps>> = ({
title,
isOpen,
onClose,
actions,
onAction,
children,
}) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50">
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
<div className="fixed inset-0 flex items-center justify-center p-4">
<div role="dialog" aria-modal className="w-full max-w-md rounded-lg bg-white shadow-xl">
<div className="border-b p-4 text-lg font-semibold">{title}</div>
<div className="p-4">{children}</div>
<div className="flex justify-end gap-2 border-t p-3">
{actions.map((a) => (
<button
key={a.type}
className="rounded-md bg-gray-200 px-3 py-2 text-sm hover:bg-gray-300"
onClick={() => onAction(a.type)}
>
{a.label}
</button>
))}
</div>
</div>
</div>
</div>
);
};
// Example code that increases every time you implement.
<AnyDialog
title="Do you delete?"
isOpen={open}
onClose={() => setOpen(false)}
actions={[
{ label: "Cancel", type: "cancel" },
{ label: "Delete", type: "confirm" },
]}
onAction={(t) => (t === "confirm" ? doDelete() : setOpen(false))}
>
This operation can not be canceled.
</AnyDialog>;
Improvement Example
When the purpose is fixed, it should be extracted as a distinct purpose-specific component (or a distinct variant). This allows the use of specific types, eliminating duplicate implementations on the calling side.
type ConfirmDialogProps = {
title: string;
isOpen: boolean;
confirmText?: string;
cancelText?: string;
destructive?: boolean;
onConfirm: () => void;
onCancel: () => void;
};
export const ConfirmDialog: React.FC<React.PropsWithChildren<ConfirmDialogProps>> = ({
title,
isOpen,
confirmText = "OK",
cancelText = "Cancel",
destructive,
onConfirm,
onCancel,
children,
}) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50">
<div className="fixed inset-0 bg-black/50" onClick={onCancel} />
<div className="fixed inset-0 flex items-center justify-center p-4">
<div role="dialog" aria-modal className="w-full max-w-md rounded-lg bg-white shadow-xl" onClick={(e) => e.stopPropagation()}>
<div className="border-b p-4 text-lg font-semibold">{title}</div>
<div className="p-4">{children}</div>
<div className="flex justify-end gap-2 border-t p-3">
<button className="rounded-md bg-gray-200 px-3 py-2 text-sm hover:bg-gray-300" onClick={onCancel}>
{cancelText}
</button>
<button
className={["rounded-md px-3 py-2 text-sm text-white", destructive ? "bg-red-600 hover:bg-red-700" : "bg-blue-600 hover:bg-blue-700"].join(" ")}
onClick={onConfirm}
>
{confirmText}
</button>
</div>
</div>
</div>
</div>
);
};
// Good example
<ConfirmDialog
title="Delete this item."
isOpen={open}
destructive
onConfirm={doDelete}
onCancel={() => setOpen(false)}
>
This operation can not be canceled.
</ConfirmDialog>;
The principle is to perform abstraction “only to the minimum necessary extent.” When the use case is limited, actively concretize props to improve readability and consistency.
Top comments (0)