Add a CSV importer to React in 10 minutes
The React package wraps the importer in a single component. Install it, describe the columns you expect, render the component, and handle the array of clean rows it hands back.
npm install dromo-uploader-react
Start with the schema. Each field has a key your backend understands, a human label the user sees during column matching, and a list of validators. Keep this in its own module so the same definition can be reused for a headless import later.
// src/import/contactFields.ts
import type { IDeveloperField } from "dromo-uploader-react";
export const contactFields: IDeveloperField[] = [
{
label: "Email",
key: "email",
type: "email",
validators: [{ validate: "required" }, { validate: "unique" }],
},
{ label: "Full name", key: "full_name", type: "string", validators: [{ validate: "required" }] },
{ label: "Phone", key: "phone", type: "string" },
{
label: "Plan",
key: "plan",
type: "select",
selectOptions: [
{ label: "Starter", value: "starter" },
{ label: "Team", value: "team" },
{ label: "Enterprise", value: "enterprise" },
],
},
];
Now render the importer. The component renders a trigger button; the modal, upload step, matching step, and review grid are all managed internally. Your only job is the onResults callback.
// src/import/ImportContactsButton.tsx
import { useCallback } from "react";
import DromoUploader from "dromo-uploader-react";
import { contactFields } from "./contactFields";
interface Props {
onImported: (rows: Record<string, unknown>[]) => void;
}
export function ImportContactsButton({ onImported }: Props) {
const handleResults = useCallback(
(data: Record<string, unknown>[]) => onImported(data),
[onImported]
);
return (
<DromoUploader
licenseKey={import.meta.env.VITE_DROMO_KEY}
fields={contactFields}
settings={{ importIdentifier: "contacts", developmentMode: true }}
user={{ id: "user_42" }}
onResults={handleResults}
>
Import contacts
</DromoUploader>
);
}
The rows that reach onImported have already passed every validator, so the parent component can POST them straight to your API without a second round of checks. In practice that is a fetch to a bulk endpoint followed by a query invalidation or a state update, and the feature is done.
How it fits the React model
The importer is a leaf in your component tree, not a page you route to. Configuration flows in through props, results flow out through a callback, and nothing about it requires global state. If you already keep the current user or tenant in a context provider, read from that provider in the parent and pass the values down; the component does not need to know about your context.
Because the fields array is plain data, you can derive it from state. A workspace with custom properties can build the schema at render time, and a feature flag can toggle a validator on or off. The uploader treats a changed fields prop as a new configuration the next time it opens.
TypeScript types ship with the package. IDeveloperField describes the schema, and the settings object is typed so that a misspelled option fails at compile time rather than at demo time. If your codebase is strict about unknown, narrow the result rows with a runtime schema library of your choice before they touch the database.
React gotchas Dromo handles for you
Unstable callback references. A file parser wired up in useEffect re-subscribes every time the parent re-renders, because the callback identity changed. Teams end up with duplicate parse events or stale closures reading old state. The uploader holds its own lifecycle, so the callback you pass is invoked once per completed import regardless of how many times the parent renders.
StrictMode double invocation. In development, React mounts, unmounts, and remounts components to surface impure effects. A hand-rolled importer that kicks off parsing on mount will parse the file twice and, on a bad day, submit it twice. Dromo does nothing on mount; the import begins only when the user acts.
The wizard state machine. Upload, header detection, column matching, error review, and submit are five states with transitions between them, plus a "go back and fix the mapping" edge that everyone forgets. Modeling that in useReducer is a week of work before any validation exists. The component owns the machine, and you receive a single terminal event.
Large files on the main thread. Parsing a 200 MB export in the render thread freezes the tab, and holding the whole thing in component state can exhaust memory. Dromo streams the file through the review grid without pinning it in your state tree, and users can download a partially fixed workbook to finish offline and resume later.
Uncontrolled file inputs. <input type="file"> cannot be controlled, so resetting it after a failed upload means juggling a key or a ref. There is no file input in your code at all.
Dromo vs building it with PapaParse
| Build it yourself | Dromo | |
|---|---|---|
| Column mapping | Write the header-matching UI, fuzzy matching, and per-user memory of past mappings | AI suggestions match headers to your schema and remember the choice between imports |
| Validation | Hand-write checks per field, plus a way to display them | Types, regex, ranges, uniqueness, allowed values, and custom validators that call your API |
| Error correction UI | Build a spreadsheet grid with inline editing, or bounce users back to Excel | Errors highlighted inline in a review grid the user edits in place |
| Large files | Web Workers, chunked parsing, and careful memory management | Handled inside the widget; users can also fix offline and re-upload to resume |
| Internationalization | Translate every label and message yourself | 30+ languages built in |
| Ongoing maintenance | Every new customer format is a ticket for your team | Configuration changes, often made by a PM in Schema Studio |
If your input is a well-formed CSV produced by a system you control, PapaParse and a hundred lines of code is the right call, and you should not pay for an importer. The line is crossed when the files come from customers: mixed headers, merged cells, Excel exports, and rows that are wrong in ways only the uploader can fix. At that point the review UI, not the parser, is the expensive part.
Ship it to your users
White labeling is included on every plan at no extra cost. Colors, fonts, and every string in the flow can be changed so the modal reads as part of your product; there is no Dromo logo to hide and no upgrade required to remove one.
Translations for more than thirty languages are built in. A customer in Brazil sees the matching step in Portuguese without your team maintaining a locale file, and the copy overrides you make apply per language.
For accounts that will not let a file leave their machine, Private Mode runs the entire import inside the browser with zero retention on Dromo's side. Results still arrive through the same onResults callback, so the React integration does not change; only a settings flag does.

