Skip to content

Import

The import wizard guides users through uploading a file, mapping columns to your schema, and resolving value mismatches. Rows then enter the grid, where users clean and edit them before submission.

TypeDataEditorFormat[] | false
DefaultAll formats

Which file formats the user can import. Set to false to disable import entirely.

importFormats={["csv", "xlsx"]}
type DataEditorFormat = "csv" | "tsv" | "xlsx" | "json" | "xml";

Shared with exportFormats. Each value maps to one or more file types:

  • csv: .csv
  • tsv: .tsv
  • xlsx: the spreadsheet token. On import it accepts .xlsx, .xls (legacy Excel), .xlsb, and .ods; on export it writes .xlsx.
  • json: .json
  • xml: .xml
TypeRemoteSource[]

Custom data sources rendered as buttons on the upload step. You own all integration complexity — auth, pickers, downloads. The SDK just calls your fetch function and processes the result.

Return a File object to go through the standard parse pipeline (CSV/XLSX/etc.), or return structured records to skip parsing entirely.

type RemoteSource = {
id: string;
label: string;
icon: string;
description?: string;
fetch: () => Promise<File | Record<string, unknown>[]>;
};
remoteSources={[
{
id: "google-sheets",
label: "Google Sheets",
icon: "<svg>...</svg>",
fetch: async () => {
const data = await myGoogleSheetsLib.pick();
return data.rows;
},
},
]}

Updog inspects the first rows of each file to decide which row holds the column headers, looking at where each column’s values switch from text labels to a consistent data type (numbers, dates, booleans). A title or metadata row above the real header is skipped automatically.

When a file contains only data and no detectable header (for example, a raw export whose first row is already values), Updog generates placeholder column names (Column 1, Column 2, …) and treats every row as data, so nothing is dropped. You then map those columns to your schema in the matching step using the previewed values.

Type(headers: string[], columns: DataEditorColumn[]) => Record<string, string | null> | Promise<...>

Override column matching during import. Called with the file’s headers and your column definitions. Return a map of { csvHeader: columnId | null }. Entries set to null or omitted fall back to built-in matching.

onColumnMatch={async (headers, columns) => {
// Call your AI or matching service
const mappings = await myMatchingService.match(headers, columns);
return mappings;
}}
Type(valuesToMatch: Record<string, ValueMatchInput>) => ValueMatchOutput | Promise<ValueMatchOutput>

Override value matching during import for select columns. Called once after column mapping with all select columns’ imported values and allowed options.

type ValueMatchInput = {
importedValues: string[];
options: string[];
};
// Return: { columnId: { importedValue: optionValue | null } }
type ValueMatchOutput = Record<string, Record<string, string | null>>;

Values set to null skip auto-matching. Unmapped values fall back to built-in fuzzy matching.

onValueMatch={async (valuesToMatch) => {
// valuesToMatch = { country: { importedValues: ["espana", "fr"], options: ["Spain", "France"] } }
return {
country: { espana: "Spain", fr: "France" },
};
}}

In the value-matching step a select value is imported only if it is mapped; values left unmatched are dropped. By default (enableCustomValue true) the user can keep an off-list value by explicitly creating a new option for it (off-list values are never added as options implicitly), and created options persist on the column. Validity is left to your validators. Set enableCustomValue: false on the column’s select editor to disable inline option creation (a strict closed enum that only maps to existing options).

A multiselect column stores a string[], so a raw cell holding several values is split into tokens before matching. The SDK auto-detects the separator among ,, ;, |, newline, and tab by checking which one splits cells into tokens that resemble your options (exact or fuzzy). Each resulting token then flows through value matching like a single select value. The value-matching step shows the detected separator and lets the user override it.

Detection needs the file’s tokens to resemble your options. When the vocabulary is disjoint (file values red, green against option codes R/G/B), set the column’s delimiter explicitly so splitting does not depend on detection. A cell that is itself a whole option containing the delimiter (option "Smith, Jr") is never split.

A cell that accidentally uses a different separator than the rest of the column (a stray ; where the column is comma-separated) is recovered automatically: a token that is not itself an option but splits on another candidate separator into pieces that all match options is split further. The recovery applies only when every resulting piece is a known option, so a value that merely contains a separator is left intact.

TypeRecord<string, string[]>

Extra synonyms layered on top of the built-ins, used by both column matching (header vs column) and value matching (imported value vs select option). Each key is the canonical target (a column ID/title, or a select option value); the array lists aliases the matching engine should treat as equivalent. Your entries are merged with the built-ins as a union per key, so you extend rather than replace them.

synonyms={{
// column aliases — incoming header → column
productSku: ["sku", "article_no", "item_code"],
firstName: ["first", "given_name", "fname"],
// value aliases — incoming cell value → select option
Active: ["live", "enabled"],
}}

Built-in value synonyms already cover common categorical vocabularies (gender M/F, seniority Jr/Sr, employment type FT/PT, status, marital status, priority, yes/no), so abbreviations like M, Jr, or FT auto-map without any configuration.

Updog stores nothing on a server. To make repeat imports auto-match the headers and values your users already mapped once, persist what they matched and feed it back through synonyms. Every onComplete result carries learnedSynonyms: { source, target } mappings the user confirmed, split into columns and values.

<DataEditor
synonyms={storedSynonyms}
onComplete={async (result) => {
const { columns, values } = result.learnedSynonyms;
await saveSynonyms([...columns, ...values]); // store the rows in your DB
await saveRows(result.sources);
}}
/>

The synonyms prop groups aliases under each canonical, so on the next mount turn your stored rows into that shape:

const synonyms = {};
for (const { source, target } of storedRows) {
(synonyms[target] ??= []).push(source);
}

The matches users made stick, so each import gets a little smarter.

Typeboolean
Defaulttrue

Allow creating new columns for unmatched headers during import. When enabled, users can keep data from columns that don’t match your schema by creating dynamic columns on the fly.