Skip to content

Hooks

Three callbacks hand the wizard over to your code at fixed points on the data’s way in. onColumnMatch answers the column mapping when the person reaches column matching, once per file. onValueMatch answers the value mapping when the person reaches value matching, once per upload. onRowImport edits the rows themselves while they land, once per chunk.

Each callback returns a plain value or a Promise, and the SDK waits up to 30 seconds. A callback that throws or runs out of time steps aside, so the built-in behavior takes over and the import keeps moving.

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

Answers the column mapping. When the person first reaches the column-matching step, the SDK calls it with that file’s headers and your column definitions. With several files, each file gets its own call and its own mapping, so the same header name can map to different columns in different files.

Return a map of { csvHeader: columnId | null }. An entry set to null or left out falls back to built-in matching for that header. When the callback throws or takes longer than 30 seconds, built-in matching maps the whole file.

onColumnMatch={async (headers, columns) => {
// headers = ["fname", "lname", "Mail"]
const mapping = await myMatchingService.match(headers, columns);
return mapping; // { fname: "firstName", lname: "lastName", Mail: "email" }
}}

The answer is taken once per file. Navigating back and forth between steps does not call the callback again. Uploading a different file or changing the header row does, on the next visit to the step.

Type (valuesToMatch: Record<string, ValueMatchInput>) => ValueMatchOutput | Promise<ValueMatchOutput>

Answers the value mapping for select, multiselect, country and currency columns. When the person first reaches the value-matching step, the SDK calls it once for the whole upload, with every list column’s imported values and allowed options. Values are collected across every file, so a column fed by Work in one file and Company in another arrives as one merged list.

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

A value set to null skips auto-matching. A value you leave out falls back to built-in fuzzy matching. When the callback throws or takes longer than 30 seconds, built-in fuzzy matching maps every value.

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

Your returned matches persist across later column-mapping edits, and navigating between steps leaves the callback alone. Adding or removing a file re-invokes it on the next visit to the step, and when that second call fails, the matches it already returned stay in place. Changing a file’s header row discards the returned matches and falls back to built-in fuzzy matching.

A country column hands you its 250 codes in options, or the codes of its only in the order you wrote them, and every imported value before the SDK reads any of them, so Deutschland reaches your callback as it stands in the file. Your answer wins over the built-in reading, a value you set to null skips it, and a value you leave out goes to the built-in reading and the typo matcher. The name behind a code comes from the browser, through the same call the SDK draws the cell with, so what your model sees matches the screen.

const name = new Intl.DisplayNames([locale], { type: "region" }).of("DE"); // "Germany"

A currency column hands you its 178 codes in options the same way, or the codes of its only, and every imported value as it stands in the file, US Dollar, $ or 840. Your answer wins over the built-in reading, and a value you leave out goes to the built-in reading and the typo matcher. A pair in synonyms.values whose target lies outside the column’s only does nothing, and the value goes to the built-in reading too.

After matching, a select value is imported only when it is mapped, and values left unmatched are dropped. A country value nobody mapped stays in the cell as text and is flagged. Your validators decide whether the result is valid.

By default the person keeps an off-list value by creating a new option for it (enableCustomValue: true), and created options persist on the column. Set enableCustomValue: false on the column’s select editor to turn inline option creation off, leaving a strict closed enum that maps only to existing options.

Type (rows: RowImportInputRow[], meta: RowImportMeta) => (Record<string, unknown> | null)[] | void | Promise<...>

Edit, drop, or add rows between the file and the editor. The SDK calls it while the import runs, once per chunk of 5,000 rows, file by file. By the time it runs, every cell has been through format reading, value matching, and the column’s transformer, so row holds the values the editor is about to receive.

Each element of rows carries both forms of the row. Both are copies, so mutating them changes nothing. Return new values instead.

type RowImportInputRow = {
// Keys are your schema column ids, values are read and transformed.
row: Record<string, unknown>;
// Keys are the file's headers, unmapped ones included,
// values are the raw cell text.
raw: Record<string, string>;
};

meta names the chunk and the file it came from.

type RowImportMeta = {
chunkIndex: number; // zero-based
chunkCount: number;
isLastChunk: boolean;
workbook: { name: string; sheetName?: string; headers: string[] };
mapping: Record<string, string | undefined>; // file header -> column id
context: unknown; // your `context` prop, untouched
signal: AbortSignal; // aborted on cancel and on the chunk timeout
};

The return value is positional. Element i answers rows[i], and a key you leave out keeps the value the file gave it. Clear a cell with an explicit empty value. A null at position i drops that row. Elements past rows.length are added as new rows at the end of the chunk. Return nothing to leave the chunk as it is. An async callback works the same way, and the SDK awaits it. Anything else in the array, an undefined element included, is a broken contract and fails the import.

Return each value the way it stands in the cell. A changed field goes through the column’s own reader, so a number is the canonical "1250.00" or a JS number, a date is the ISO "2026-08-18", and a currency or country resolves what its reader recognises, "usd" to "USD". What the reader cannot settle stays in the cell as text for your validators to flag. A select stores the text you return, a multiselect takes an array of values, and the column’s transformer runs on the result once. The file’s value matching applies only to the file’s own values, never to yours. Untouched fields keep their values, and their transformers run once. Keys that match no schema column are dropped.

onRowImport={(rows) =>
rows.map(({ raw }) => {
// The file holds "USD 100" in one column, your schema wants two.
const [currency, amount] = (raw["price"] ?? "").split(" ");
return { currency, amount };
})
}

When the callback throws, takes longer than 30 seconds on a chunk, or breaks the contract of the returned array, the import fails the way any failed import does. Chunks that already landed stay, the rest of the file is restored, the wizard stays open for a retry, and one HOOK_ERROR reaches onError with the cause. meta.signal aborts when the person cancels the import and when the chunk timeout fires, so hand it to your own fetch.

Rows the callback drops count as its decision, so a file it empties lands as no rows and shows no warning. The values it returns go through your validators the same way file values do. The callback returns values and never sets messages on cells. loadData, pasting, and edits in the grid leave it alone. It runs only on import.

A column the callback reads from raw needs no match. When onRowImport is set, the hint on an unmatched row says the import can read the column without a match, in place of the warning that unmatched data is lost.

Mark a field the callback fills entirely with mappable: false. Auto-matching stops proposing it, so a Full name header no longer lands on firstName, and a hidden select column asks for no value matching. Hide only fields no file ever carries directly. A hidden field cannot receive a match when a file arrives with it already split.

Type unknown

An object of yours, passed through untouched. onRowImport receives it as meta.context, so one hook function can serve every tenant, user, or preloaded lookup table your app distinguishes.

The value is captured when the wizard opens. To hand the hook data that changes while the wizard stays open, pass a container object and change its fields.