How to Import a CSV File Into a React App With the Updog Importer
This guide shows you how to add a CSV and Excel importer to a React app. Import needs more than a parser. Read CSV import is more than parsing a file to see why.
The example is an employee importer with seven columns. A messy file goes in. Clean rows come out. You install the package, describe your data, match the file to your schema, and get the result back.
Step 1. Install the package
npm install @updog/data-editor You can find the package on npm at @updog/data-editor. There is also a web component build at @updog/data-editor-wc, in case you are not coming from the React ecosystem.
Step 2. Mount the editor
Render DataEditor and set variant to uploader. This opens the import wizard first. The default editor variant opens the grid first, for cases when you already have data, for example.
Import usually sits behind a button, and modal is the default. Control it with open and onClose.
import { useCallback, useState } from "react";
import { DataEditor } from "@updog/data-editor";
export function ImportEmployees() {
const [open, setOpen] = useState(false);
const openImporter = useCallback(() => {
setOpen(true);
}, []);
const closeImporter = useCallback(() => {
setOpen(false);
}, []);
const onComplete = useCallback((result) => {
console.log(result);
}, []);
return (
<>
<button onClick={openImporter}>Import employees</button>
<DataEditor
apiKey="your-license-key"
variant="uploader"
open={open}
onClose={closeImporter}
onComplete={onComplete}
/>
</>
);
} Get the apiKey by signing up at console.updog.tech. After you sign up, you get a default API key right away. Updog is free to use on localhost and on development domains. When you are ready to go to production, each production domain costs $19 a month. You also get one bonus domain free, for your staging environment if you have one.
columns and primaryKey are required, and the next steps build both.
If you want to embed the editor in the page instead, set mode to inline and drop open and onClose.
These examples use React, the native package. For Vue, Angular, Svelte, or plain JavaScript, Updog ships as a web component with the same props. See the CSV importer for any framework page.
Step 3. Describe your data
The columns array describes the shape your app expects. Every uploaded file gets changed to match it. The more detail you add to the schema, the less anyone has to fix by hand later.
Start with the columns
Each column needs an id that matches the key in your row data. It also needs a title, the text users see. size is optional. Add it in pixels when you want a column wider or narrower.
import type { DataEditorColumn } from "@updog/data-editor";
export const columns: DataEditorColumn[] = [
{
id: "firstName",
title: "First name",
size: 150,
},
{
id: "lastName",
title: "Last name",
size: 150,
},
{
id: "email",
title: "Email",
size: 260,
},
{
id: "startDate",
title: "Start date",
size: 140,
},
{
id: "salary",
title: "Salary",
size: 120,
},
{
id: "skills",
title: "Skills",
size: 220,
},
{
id: "status",
title: "Status",
size: 140,
},
]; Choose an editor for each column
Text is the default, so the name and email columns need nothing. The other columns each need an editor. Use date for the start date, number for salary, multiselect for skills, and select for status. An editor sets what users can type. For select and multiselect, it also tells the import which values to match against a known list.
{
id: "startDate",
title: "Start date",
size: 140,
editor: {
type: "date",
},
},
{
id: "salary",
title: "Salary",
size: 120,
editor: {
type: "number",
decimalPlaces: 0,
},
},
{
id: "skills",
title: "Skills",
size: 220,
editor: {
type: "multiselect",
options: ["React", "TypeScript", "Node", "Design"],
},
},
{
id: "status",
title: "Status",
size: 140,
editor: {
type: "select",
options: ["Active", "Onboarding", "On leave", "Terminated"],
},
}, Add the rules
Validators run on every edit. A value that fails is highlighted in the grid. Here is the full schema, with editors and validators together.
import type { DataEditorColumn } from "@updog/data-editor";
const STATUSES = ["Active", "Onboarding", "On leave", "Terminated"];
export const columns: DataEditorColumn[] = [
{
id: "firstName",
title: "First name",
size: 150,
validators: [
{
type: "required",
message: "First name is required",
},
],
},
{
id: "lastName",
title: "Last name",
size: 150,
validators: [
{
type: "required",
message: "Last name is required",
},
],
},
{
id: "email",
title: "Email",
size: 260,
unique: true,
validators: [
{
type: "required",
message: "Email is required",
},
{
type: "email",
message: "Enter a valid email address",
},
],
},
{
id: "startDate",
title: "Start date",
size: 140,
editor: {
type: "date",
},
validators: [
{
type: "date",
message: "Enter a valid date",
},
],
},
{
id: "salary",
title: "Salary",
size: 120,
editor: {
type: "number",
decimalPlaces: 0,
},
validators: [
{
type: "range",
min: 0,
message: "Salary must be zero or greater",
},
],
},
{
id: "skills",
title: "Skills",
size: 220,
editor: {
type: "multiselect",
options: ["React", "TypeScript", "Node", "Design"],
},
},
{
id: "status",
title: "Status",
size: 140,
editor: {
type: "select",
options: STATUSES,
},
validators: [
{
type: "oneOf",
values: STATUSES,
message: "Select a status from the list",
},
],
},
]; Email uses required and email. unique flags a duplicate address in the file. Start date runs a date check. Salary uses a range check that rejects anything below zero. Status uses oneOf against the same list its dropdown offers, which catches a value that slips past matching. A failed rule marks the cell, but it does not block submission by default. Users can still fix it in the grid, and invalid rows still reach you, tagged with the isValid flag. If you want to hold submission until every error is fixed, set blockSubmitOnError.
Fix common errors before anyone sees them
A validator flags a bad value, but a transformer fixes it first. Add one to a column, and Updog runs it on each cell right after the user clicks import on the upload step, before the rows reach the editor grid. This lets you clean up common mistakes before users ever see them, such as trimming stray spaces, lowercasing an email, or dropping a currency symbol. It runs on text, number, and date columns, after Updog normalizes the raw value and before it enters the store. Select and multiselect columns go through value matching instead.
{
id: "email",
title: "Email",
size: 260,
transformer: (value) => String(value).trim().toLowerCase(),
}, Shape how columns show and filter
Two more column props work on the grid itself, once the file is loaded. A formatter changes how a value looks without changing what you store. Salary can show with a $ in front while the stored value stays untouched. A filter adds a control to the sidebar Filters panel. That means a dropdown for status, a min and max range for salary, and a date range for the start date.
{
id: "salary",
title: "Salary",
size: 120,
formatter: (value) => (value ? "$" + value : ""),
filter: {
type: "number-range",
label: "Salary",
},
}, Step 4. Help Updog match the file
The schema is ready, but the file rarely matches it word for word. Headers and values come in whatever words the file's author chose. Updog runs fuzzy matching first, and it already catches most small differences on its own, like extra spaces, different capitalization, or a common alternate spelling. If your data uses terms Updog cannot guess, like an internal code, an abbreviation, or a word specific to your industry, you add your own dictionary on top of it. Two props do this. synonyms lets you list the exact words you already know users use. The onColumnMatch and onValueMatch callbacks let you match anything else with your own code or your own AI model.
Match the columns
The file might say E-mail, Employment Status, and Start Date. Your columns are email, status, and startDate. Built-in matching pairs the obvious ones. Add the headers users often send to synonyms, and they map on their own.
<DataEditor
synonyms={{
email: ["e-mail", "email address", "work email"],
status: ["employment status", "employee status", "state"],
}}
...
/> For a header no list catches, pass the decision to your own model through onColumnMatch. Updog gives it the file's headers and your columns. You return a map of which goes where.
<DataEditor
onColumnMatch={async (headers, columns) => {
// headers = ["Emp Email", "Emp Status", ...] from the file
// columns = your firstName, lastName, email, ... schema
const map = await askYourModel(headers, columns);
return map; // { "Emp Email": "email", "Emp Status": "status" }
}}
...
/> See bring your own AI to CSV and Excel import for that path and how the data stays private.
Match the values
The same works inside the status column. A file can send loa, on-leave, and leave for the status On leave. Extend synonyms with value aliases. The key is the option, and the array lists what maps to it.
<DataEditor
synonyms={{
"On leave": ["loa", "on-leave", "leave"],
"Terminated": ["term", "terminated", "left"],
}}
...
/> For values no list covers, onValueMatch is the column callback one level down. Updog hands you the distinct values found in each select column, plus the options you allow. You return the mapping.
<DataEditor
onValueMatch={async (valuesToMatch) => {
// valuesToMatch = { status: { importedValues: ["loa", "left"], options: ["Active", "On leave", ...] } }
const map = await askYourModel(valuesToMatch);
return map; // { status: { loa: "On leave", left: "Terminated" } }
}}
...
/> It can run through your model too, as shown in the bring your own AI post.
Set the primary key
primaryKey decides how new rows join the rows you already have. It is a DataEditor prop, like columns. For employees, email fits well. Updog checks each imported row against the loaded rows. A match updates that employee instead of adding a copy. Import the same list twice, and you get one list, not duplicates.
Updog leans on it wherever rows have to line up. In the upload flow, it sets the default on the primary key step, where users can confirm it or choose another column. With AI chat on, it is the key used to match the rows the assistant returns, so a transformation merges into the grid instead of duplicating rows.
Step 5. Take back the result
On submit, Updog passes the edited data to onComplete, grouped by source. Each row carries four flags. They are isNew, isChanged, isDeleted, and isValid. Routing depends on your backend, so Updog leaves that part to you. Filter on the flags, and send each row where it belongs. Pass your row type to DataEditor, and the result is typed throughout.
type Employee = {
firstName: string;
lastName: string;
email: string;
startDate: string;
salary: string;
skills: string[];
status: string;
};
const onComplete = useCallback(
async (result: DataEditorResult<Employee>) => {
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,
);
await saveEmployees(inserts, updates);
}
},
[],
);
<DataEditor<Employee>
apiKey="your-license-key"
variant="uploader"
columns={columns}
primaryKey="email"
onComplete={onComplete}
/> A file went in. Typed, validated employees come out, ready to save.
Step 6. Localize it
Updog translates. Set locale, pass a translations object with the strings you override, and set rtl for right-to-left languages.
<DataEditor
locale="ar"
rtl
translations={arabicTranslations}
...
/> You do not translate every string by hand. Copy the defaults object from the localization docs, paste it into your AI, and ask it to translate the values. Drop the result into the translations prop.
What you built
You installed a package, wrote a schema, and added a few props and a result handler. It runs in the browser, so you do not need a server, and nothing leaves the page until you save it. The config you wrote handles the messy files. You handle what happens to the clean, typed rows.