Inline row actions
A per-row ⋮ menu with edit, duplicate, and delete. Destructive actions show a confirmation modal before committing. State is updated optimistically — no network call needed to see the result.
Name
Department
Role
The actions column
Use button: true on the column so clicks don't bubble to
onRowClicked, and keep the width tight so it doesn't crowd other columns:
{
name: '',
button: true,
width: '48px',
cell: row => <RowMenu row={row} onEdit={openEdit} onDelete={openDelete} />,
} Closing the dropdown on outside click
Track open state per-row inside the menu component and close on a document
mousedown that falls outside the ref:
function RowMenu({ row, onEdit, onDelete }) {
const [open, setOpen] = useState(false);
const ref = useRef(null);
useEffect(() => {
if (!open) return;
const handle = e => {
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
};
document.addEventListener('mousedown', handle);
return () => document.removeEventListener('mousedown', handle);
}, [open]);
return (
<div ref={ref}>
<button onClick={e => { e.stopPropagation(); setOpen(o => !o); }}>⋮</button>
{open && (
<div className="dropdown">
<button onClick={() => { setOpen(false); onEdit(row); }}>Edit</button>
<button onClick={() => { setOpen(false); onDelete(row); }}>Delete</button>
</div>
)}
</div>
);
} Confirmation before delete
Keep a modal state at the table level. The menu sets it; the modal reads it
and calls the commit function on confirm:
type Modal = { type: 'none' } | { type: 'delete'; row: Employee };
const [modal, setModal] = useState<Modal>({ type: 'none' });
function commitDelete(row: Employee) {
setData(prev => prev.filter(r => r.id !== row.id));
setModal({ type: 'none' });
}
{modal.type === 'delete' && (
<ConfirmDialog
message={`Delete ${modal.row.name}?`}
onConfirm={() => commitDelete(modal.row)}
onCancel={() => setModal({ type: 'none' })}
/>
)}