GuidesTutorial

How to Add CSV Import to Your React App in 5 Minutes

Install one package, render one component, handle one callback. Here is the complete working code — plus what to change before you ship it to real users.

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
  1. The short answer
  2. What you will build
  3. Step 1: Install the adapter
  4. Step 2: Render the component
  5. Step 3: Understand where the data actually goes
  6. Step 4: Make it production-ready
  7. Where to go next
  8. Frequently asked questions

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#

Shell
npm install @csvbox/react

Then 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.

JSX
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>
  )
}
components/ImportCustomers.jsx

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. result is a boolean; data carries the run metadata including row_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.

ConcernHandled by
Parsing, encoding, delimitersCSVbox importer
Column mapping UICSVbox importer
Validation and inline correctionCSVbox importer, per your sheet schema
Delivering rows to your systemYour configured destination
Knowing the import finishedThe onImport callback in React
Refreshing your UI afterwardsYour code, in onImport
What each side is responsible for

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

JSX
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 />}
    </>
  )
}
Tracking state across the whole import

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.

JSX
<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>
  )}
/>
Custom trigger with lazy loading

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.

Topicsreacttutorialintegration

Stop building CSV importers.

Ship ours in 15 minutes. Free forever on the Sandbox plan.

No credit cardEmbed in minutesSecure by default