Submitting
When the user clicks Submit, Updog hands you the edited dataset grouped by source. You filter on the per-row flags and decide what each row becomes: an INSERT, an UPDATE, a DELETE, or nothing.
onComplete
Section titled “onComplete”| Type | (result: DataEditorResult<TRow>) => void | Promise<void> |
DataEditorResult
Section titled “DataEditorResult”type DataEditorResult<TRow> = { sources: DataEditorSourceResult<TRow>[]; counts: { new: number; changed: number; deleted: number; invalid: number; }; learnedSynonyms: LearnedSynonyms;};
type LearnedSynonym = { source: string; // the imported header or cell value target: string; // the canonical it was mapped to (column title or option)};
type LearnedSynonyms = { columns: LearnedSynonym[]; // header → column matches values: LearnedSynonym[]; // cell value → option matches};counts is an aggregate across every source.
learnedSynonyms holds the mappings the user changed by hand in the wizard, split into columns and values. Each entry is one 1:1 mapping, ready to store as a row. Both lists are [] when nothing new was learned.
Four kinds of mapping stay out of it:
- mappings the SDK matched on its own, which it will match again next time
- mappings returned by
onColumnMatchoronValueMatch, since that answer already lives in your code - pairs the built-in table or your own
synonymsalready covers - everything from an import the user cancelled
Persist them and feed them back through synonyms next time so repeat imports auto-match. See Remembering matches across imports.
DataEditorSourceResult
Section titled “DataEditorSourceResult”type DataEditorSourceResult<TRow> = { sourceId: string; sourceName: string; rows: ResultRow<TRow>[];};One entry per source. A backend row is omitted when nothing about it actually changed, because writing it back would be a no-op. That covers rows the user left alone and undeleted, and rows an import matched on the primary key and rewrote with the values they already held.
An import contributes one entry per file, plus one per sheet when a multi-sheet workbook is imported (sourceName is then workbook.xlsx - Sheet name). A user who uploads three files in one pass produces three entries, so expect result.sources to hold several import entries alongside whatever you tagged through loadData. See Several files in one import.
ResultRow
Section titled “ResultRow”type ResultRow<TRow> = { row: TRow; isNew: boolean; isChanged: boolean; isDeleted: boolean; isValid: boolean;};The flags are independent. A single row can be new, changed, and deleted at once.
isChanged compares values, not the way they are written. An empty cell that stayed empty is not a change, whichever form the emptiness took — an empty string, null, a missing field, or a multiselect with nothing chosen. A number and its text form are the same value too, so 1 rewritten as "1" leaves the row unchanged. Text that reads "null" stays ordinary text.
The row you get back carries one key per column in your schema. A column your user’s file
never held arrives as an empty cell — "", or [] for a multiselect — rather than as a
missing field, so Object.keys(row) is the same set for every row in the result. Values you
loaded yourself come back as you wrote them, null included.
Updog leaves the rows ungrouped because routing depends on your backend. Some clients upsert, some reject invalid rows, some keep deletions for audit.
Example
Section titled “Example”onComplete={async (result) => { for (const source of result.sources) { const inserts = source.rows.filter(r => r.isNew && !r.isDeleted && r.isValid); const updates = source.rows.filter(r => !r.isNew && r.isChanged && !r.isDeleted && r.isValid); const deletes = source.rows.filter(r => r.isDeleted && !r.isNew);
await persist(source.sourceId, { inserts, updates, deletes }); }}}If you never tagged sources through loadData, you get one entry with sourceId: "backend".
What a file export carries
Section titled “What a file export carries”An export writes the stored form, not what the grid paints. A number leaves as plain digits with a dot for decimals and no grouping; a date leaves as ISO YYYY-MM-DD. JSON and XLSX carry a number column’s value typed, so a machine on the other end reads a number rather than re-parsing a locale. A value the SDK flags leaves as the raw text the user still has to fix.
column.formatter applies to the grid, so an export skips it: a column storing 1200 behind a (v) => "$" + v formatter writes 1200.
