The short answer
To add CSV import to a React app, install @csvbox/react, then render the CSVBoxButton component with your sheet licence key, a user object, and an onImport callback. The importer opens in a modal, handles parsing, column mapping, and validation, and delivers validated rows to the destination configured in your dashboard.
Key takeaways
- One package, one component, one callback — the working version is about fifteen lines.
- Your React code never receives the file. Rows go to the destination you configure in the dashboard.
- onImport is for UI feedback, not data delivery. Do not build your write path around it.
- The five-minute version is real, but scope the user object and move the licence key to an env var before shipping.
On this page
What you will build#
A button in your React app that opens a full import flow: the user drops in a CSV or Excel file, maps their columns to your schema, sees validation errors inline and fixes them, and submits. Clean rows arrive at your API. Your React code handles none of that — it renders a button and reacts to the result.
You need a CSVbox account (the Starter plan is free), a sheet defined in the dashboard, and a React app. Works with Vite, Next.js, Create React App, or Remix — the adapter is a plain client component with no bundler-specific requirements.
Step 1: Install the adapter#
npm install @csvbox/reactThen copy the sheet licence key from your dashboard. It is per-sheet, not per-account, so a project with separate importers for customers and products has two keys.
Step 2: Render the component#
This is the whole integration. CSVBoxButton renders its children as the trigger and manages the modal itself.
import { CSVBoxButton } from '@csvbox/react'
export default function ImportCustomers() {
return (
<CSVBoxButton
licenseKey={import.meta.env.VITE_CSVBOX_KEY}
user={{ user_id: 'default123' }}
onImport={(result, data) => {
if (result) {
console.log(data.row_success + ' rows imported')
} else {
console.log('Import failed')
}
}}
>
Import customers
</CSVBoxButton>
)
}Three props do the work:
licenseKey— which sheet, and therefore which schema and which destination.user— an object identifying who is importing. It is passed through to your destination with every batch, which is how you know whose data just arrived.onImport(result, data)— fires when the import finishes.resultis a boolean;datacarries the run metadata includingrow_success.
Step 3: Understand where the data actually goes#
This is the part people get wrong, so it is worth being explicit: your React code never receives the rows.
Validated data is delivered to the destination you configured in the dashboard — your API endpoint, a database, Google Sheets, an automation platform, whatever you selected. That delivery happens server-to-server. The onImport callback tells your UI that it finished; it is not the transport.
| Concern | Handled by |
|---|---|
| Parsing, encoding, delimiters | CSVbox importer |
| Column mapping UI | CSVbox importer |
| Validation and inline correction | CSVbox importer, per your sheet schema |
| Delivering rows to your system | Your configured destination |
| Knowing the import finished | The onImport callback in React |
| Refreshing your UI afterwards | Your code, in onImport |
The practical consequence: build your write path against your destination endpoint and treat onImport purely as a UI signal. If you make the callback responsible for persistence, a user closing the tab mid-import loses data that CSVbox already delivered successfully.
Step 4: Make it production-ready#
Four changes take this from a demo to something you can ship.
Scope the user object to the real user
The user_id: "default123" in every quickstart is a placeholder. Replace it with your actual identifiers — this object is what lets your destination attribute rows to the right account, and it is how you filter imports in the dashboard when someone reports a problem.
Wire up the full lifecycle
import { useState } from 'react'
import { CSVBoxButton } from '@csvbox/react'
export default function ImportCustomers({ user, onComplete }) {
const [status, setStatus] = useState('idle')
return (
<>
<CSVBoxButton
licenseKey={import.meta.env.VITE_CSVBOX_KEY}
user={{
user_id: user.id,
team_id: user.teamId,
email: user.email,
}}
onReady={() => setStatus('ready')}
onSubmit={() => setStatus('importing')}
onImport={(result, data) => {
setStatus(result ? 'done' : 'error')
if (result) onComplete(data)
}}
onClose={() => setStatus('idle')}
>
Import customers
</CSVBoxButton>
{status === 'importing' && <Spinner label="Importing your data…" />}
{status === 'error' && <ErrorNotice />}
</>
)
}onReady fires when the importer has loaded, onSubmit when the user commits, onClose when they dismiss the modal. Handling onClose matters more than it looks — without it, a user who opens the importer and backs out leaves your UI stuck in a loading state.
Use your own button
The default trigger will not match your design system. The render prop hands you the launch function and a loading flag so you can render whatever you like, and lazy defers loading the importer until it is actually needed.
<CSVBoxButton
licenseKey={import.meta.env.VITE_CSVBOX_KEY}
user={{ user_id: user.id }}
onImport={handleImport}
lazy
render={(launch, isLoading) => (
<Button onClick={launch} disabled={isLoading} variant="primary">
{isLoading ? 'Loading…' : 'Import customers'}
</Button>
)}
/>Refresh your data when it finishes
Because rows arrive at your destination out of band, your React state has no idea anything changed. Invalidate the relevant query in onImport — with TanStack Query that is a queryClient.invalidateQueries call, and with SWR a mutate. Skipping this produces the single most common complaint about embedded importers: “it said it worked but nothing appeared”.
Where to go next#
On Next.js the App Router needs one extra thing from you — see the Next.js guide. To understand what the mapping screen is doing under the hood, read how AI column mapping works.
And if you are still weighing this against writing your own, the hidden costs covers what the build estimate leaves out.
Frequently asked questions
Does @csvbox/react work with Next.js?
Yes. The importer is a browser component, so in the App Router it must live in a file marked "use client" — or be imported by one. In the Pages Router it works without changes. Use NEXT_PUBLIC_CSVBOX_KEY for the licence key so it is available client-side.
How do I get the imported rows into my React state?
You do not, directly — rows are delivered server-to-server to the destination configured in your dashboard. In the onImport callback, refetch from your own API rather than reading rows from the callback. Treat onImport as a signal that your data changed, then invalidate the relevant query.
Can I define the import schema in code instead of the dashboard?
Yes, via the dynamicColumns prop, which sets the sheet schema at runtime. This is the right approach when the expected columns vary per customer or per workspace. For a fixed schema, defining it in the dashboard is simpler and keeps validation rules out of your bundle.
What does the data argument in onImport contain?
Run metadata about the import rather than the rows themselves — including row_success, the count of successfully imported rows. Use it for confirmation messaging. Do not depend on it as your record of what was imported; your destination is the source of truth.
Is the licence key safe to expose in client-side code?
Yes. It identifies which sheet to open and does not grant write access to your systems — data delivery happens through the destination you configured, not through anything the browser can reach. Still keep it in an environment variable so staging and production can point at different sheets.
Does this work with React Native?
No. @csvbox/react targets the DOM and renders the importer in an iframe modal, so it requires a web environment. For React Native, open the hosted importer in a web view or build the upload against your API directly.