onComplete
onComplete: (rows: Record<string, unknown>[], meta: ImportMeta) => void | Promise<void>
type ImportMeta = {
validRows: number;
invalidRows: number;
skippedRows?: number;
fileType: "csv" | "xlsx" | "tsv" | "xlsb" | "ods" | "json";
durationMs: number;
columnCount: number;
rowCount: number;
sheetName?: string; // set for Excel imports
headerRow: number; // 1-based spreadsheet row used as headers
upsert?: { primaryKey: string; newRows: number; changedRows: number; missingKeyRows: number };
};rows are objects keyed by field key, with coerced types (numbers as numbers, emails lowercased, dates as YYYY-MM-DD, booleans as booleans, URLs as normalized http/https hrefs, empty optional fields as null). Invalid rows are omitted.
Pass primaryKey and existingKeys (the host's prior ids) to receive meta.upsert counts for new vs already-known vs blank-key rows. There is no multi-file merge grid. Keys never leave the browser except through your onComplete.
If onComplete throws or returns a rejected Promise, the widget shows that message (the Error text when present) and re-enables Import. A successful callback still disables Import until the user goes back to mapping, so a second click cannot duplicate the send.
Your backend should re-check authorization and any server-side uniqueness against your database. Client validation is not a security boundary.
Related: Quickstart · React component · Import CSV to Postgres · Sessions