Blog
January 11, 2026

Add CSV import to Retool: custom component guide

Learn how to add CSV import functionality to Retool apps using both native file inputs and custom components with ImportCSV for column mapping and validation.

11 mins read

Add CSV import to Retool: custom component guide

Retool makes building internal tools fast, but CSV imports often become a pain point. Users need to upload spreadsheets, and you need to get that data into your database without corruption or errors.

Retool provides native file input components that work for basic uploads. However, they lack column mapping, validation, and error handling features that production apps often require. This guide covers both approaches: using Retool's built-in capabilities for simple use cases, and building a custom component with ImportCSV when you need more control.

Prerequisites

Before starting, you'll need:

  • A Retool account (Cloud or Self-hosted)
  • Node.js v20 or later (for custom components)
  • Admin permissions in Retool (for custom component deployment)
  • An API access token with Custom Component Libraries read/write scopes

Retool's native CSV capabilities

Retool provides three file input components out of the box:

  • File Button - A button-style file upload
  • File Dropzone - A drag-and-drop area for file upload
  • File Input - A traditional file input field

Setting up native CSV upload

The quickest way to add CSV import is with a File Button component:

  1. Drag a File Button component onto your canvas
  2. In the Inspector panel, enable Parse files
  3. Access parsed data via fileButton1.parsedValue[0]

According to Retool's documentation: "Retool can parse uploaded text files (any file with a text/plain media type) and make the content available for use in the app."

Displaying uploaded data

Add a Table component and set its data source to the parsed file:

{{ fileButton1.parsedValue[0] }}

The table will display the CSV data with automatic column detection.

Writing to database

Create a query to insert the uploaded data. In GUI mode:

  1. Create a new query for your database
  2. Set the action type to Bulk insert
  3. Reference the table data: {{ table1.data }}

For row-by-row insertion, loop through selected rows:

{{ table1.selectedRow }}

Native approach limitations

Based on Retool community forum discussions, the native file components have several limitations:

LimitationDetails
File size limit3MB for CSV import tool
No column mapping UIManual mapping required
No data validationMust build custom validation
No error previewErrors appear on insert
Null value handlingIssues with nullable columns
Numeric parsingBoolean and decimal errors common

Users on the Retool forums report issues like "Error importing numerics in CSV file" and "Import CSV to retool database gives error on null columns." If your use case involves larger files, complex data types, or user-facing imports where data quality matters, a custom component approach provides more control.

When to use a custom component

A custom component makes sense when you need:

  • Large file handling - Files over 3MB
  • Column mapping - Users match CSV columns to your schema
  • Data validation - Check data types, required fields, formats before insert
  • Error preview - Show which rows have problems before committing
  • Type coercion - Automatic conversion of strings to proper data types

The tradeoff is setup time: native components take minutes, while a custom component takes about 30 minutes to configure and deploy.

Architecture overview

The custom component approach works like this:

  1. Your React component wraps ImportCSV and handles file selection
  2. ImportCSV provides column mapping UI and validation
  3. Parsed data passes to Retool via state variables
  4. Event callbacks notify your Retool app when import completes
  5. A Retool query inserts the validated data into your database
[User drops CSV] -> [Custom Component] -> [ImportCSV processes]
                                              |
                                              v
                        [Retool State] <- [Validated data]
                                              |
                                              v
                        [Retool Query] -> [Database]

Step 1: Set up custom component development

Retool custom components are React components bundled and deployed to your Retool instance. Start by cloning the template:

git clone https://github.com/tryretool/custom-component-collection-template
cd custom-component-collection-template
npm install

Log in to your Retool instance:

npx retool-ccl login

This prompts for your Retool URL and API access token. The token needs Custom Component Libraries read and write scopes.

Create a new component library:

npx retool-ccl init

Follow the prompts to name your library.

Step 2: Install ImportCSV

Add ImportCSV to your component dependencies:

npm install @importcsv/react

Step 3: Create the CSV importer component

Replace the contents of src/components/HelloWorld.tsx (or create a new file) with:

import { FC, useState } from 'react';
import * as Retool from '@retool/custom-component-support';
import { CSVImporter } from '@importcsv/react';

export const CSVImporterComponent: FC = () => {
  // State to pass data back to Retool
  const [importedData, setImportedData] = Retool.useStateArray({
    name: 'importedData',
    initialValue: [],
    inspector: 'hidden',
  });

  const [importStatus, setImportStatus] = Retool.useStateObject({
    name: 'importStatus',
    initialValue: { success: false, rowCount: 0, error: null },
    inspector: 'hidden',
  });

  // Event callback to notify Retool when import completes
  const onImportComplete = Retool.useEventCallback({
    name: 'importComplete',
  });

  // Configurable schema from Retool inspector
  const columns = Retool.useStateArray({
    name: 'columns',
    initialValue: [
      { name: 'email', label: 'Email', required: true },
      { name: 'name', label: 'Full Name', required: true },
      { name: 'company', label: 'Company', required: false },
    ],
    inspector: 'text',
    label: 'Column Schema (JSON)',
  });

  // Component sizing
  Retool.useComponentSettings({
    defaultWidth: 8,
    defaultHeight: 40,
  });

  const handleComplete = (data: Record<string, unknown>[]) => {
    setImportedData(data);
    setImportStatus({
      success: true,
      rowCount: data.length,
      error: null,
    });
    onImportComplete();
  };

  const handleError = (error: Error) => {
    setImportStatus({
      success: false,
      rowCount: 0,
      error: error.message,
    });
  };

  return (
    <div style={{ height: '100%', width: '100%' }}>
      <CSVImporter
        columns={columns}
        onComplete={handleComplete}
        onError={handleError}
      />
    </div>
  );
};

Update the component index to export your new component. In src/index.tsx:

import { CSVImporterComponent } from './components/CSVImporterComponent';

export const components = [
  {
    Component: CSVImporterComponent,
    name: 'CSVImporter',
  },
];

Step 4: Configure component settings

The component exposes several pieces of state that your Retool app can access:

  • csvImporter.importedData - Array of parsed rows as objects
  • csvImporter.importStatus - Object with success, rowCount, and error fields
  • csvImporter.columns - The column schema (configurable in Inspector)

The importComplete event fires when users finish the import flow.

Step 5: Test in development mode

Start the development server:

npx retool-ccl dev

This starts a local server and provides instructions for adding the development component to a Retool app:

  1. Open your Retool app in edit mode
  2. Open the Component Library panel
  3. Click "Add custom component"
  4. Select your development library

The component updates live as you make changes.

Step 6: Deploy to production

When the component works correctly, deploy it:

npx retool-ccl deploy

This bundles your component and uploads it to Retool. The deployed component is available in all apps in your Retool organization.

Note the limits: maximum 10MB per library revision (30MB in dev mode), and 5GB total for all custom component libraries on Retool Cloud.

Step 7: Use in your Retool app

Add the deployed custom component to your app:

  1. Open the Component Library panel
  2. Find your CSV Importer component
  3. Drag it onto the canvas
  4. Configure the column schema in the Inspector

Configure columns

Set the columns property in the Inspector to match your database schema:

[
  { "name": "email", "label": "Email Address", "required": true },
  { "name": "first_name", "label": "First Name", "required": true },
  { "name": "last_name", "label": "Last Name", "required": true },
  { "name": "phone", "label": "Phone Number", "required": false },
  { "name": "company", "label": "Company", "required": false }
]

Handle the import event

Create an event handler for the importComplete event:

  1. Select the CSV Importer component
  2. In the Inspector, find Event handlers
  3. Add a handler for importComplete
  4. Set action to Run query and select your insert query

Create the database insert query

Create a query that inserts the imported data. For PostgreSQL:

INSERT INTO users (email, first_name, last_name, phone, company)
SELECT
  item->>'email',
  item->>'first_name',
  item->>'last_name',
  item->>'phone',
  item->>'company'
FROM json_array_elements({{ JSON.stringify(csvImporter.importedData) }}::json) AS item

For bulk insert in GUI mode:

  1. Create a new query
  2. Select Bulk insert action
  3. Set Records to insert to {{ csvImporter.importedData }}

Complete working example

Here's the full custom component with additional features like progress tracking:

import { FC, useState } from 'react';
import * as Retool from '@retool/custom-component-support';
import { CSVImporter } from '@importcsv/react';

interface Column {
  name: string;
  label: string;
  required: boolean;
  type?: 'string' | 'number' | 'date' | 'boolean';
}

export const CSVImporterComponent: FC = () => {
  const [importedData, setImportedData] = Retool.useStateArray({
    name: 'importedData',
    initialValue: [],
    inspector: 'hidden',
  });

  const [validationErrors, setValidationErrors] = Retool.useStateArray({
    name: 'validationErrors',
    initialValue: [],
    inspector: 'hidden',
  });

  const [importStatus, setImportStatus] = Retool.useStateObject({
    name: 'importStatus',
    initialValue: {
      success: false,
      rowCount: 0,
      errorCount: 0,
      error: null,
    },
    inspector: 'hidden',
  });

  const onImportComplete = Retool.useEventCallback({
    name: 'importComplete',
  });

  const onValidationError = Retool.useEventCallback({
    name: 'validationError',
  });

  const columns = Retool.useStateArray({
    name: 'columns',
    initialValue: [
      { name: 'email', label: 'Email', required: true, type: 'string' },
      { name: 'name', label: 'Full Name', required: true, type: 'string' },
      { name: 'amount', label: 'Amount', required: false, type: 'number' },
    ] as Column[],
    inspector: 'text',
    label: 'Column Schema (JSON)',
  });

  const primaryColor = Retool.useStateString({
    name: 'primaryColor',
    initialValue: '#2563eb',
    inspector: 'text',
    label: 'Primary Color',
  });

  Retool.useComponentSettings({
    defaultWidth: 8,
    defaultHeight: 50,
  });

  const handleComplete = (data: Record<string, unknown>[]) => {
    setImportedData(data);
    setValidationErrors([]);
    setImportStatus({
      success: true,
      rowCount: data.length,
      errorCount: 0,
      error: null,
    });
    onImportComplete();
  };

  const handleError = (error: Error) => {
    setImportStatus({
      success: false,
      rowCount: 0,
      errorCount: 1,
      error: error.message,
    });
  };

  const handleValidationError = (errors: Array<{ row: number; message: string }>) => {
    setValidationErrors(errors);
    setImportStatus({
      success: false,
      rowCount: 0,
      errorCount: errors.length,
      error: `${errors.length} validation errors found`,
    });
    onValidationError();
  };

  return (
    <div style={{
      height: '100%',
      width: '100%',
      fontFamily: 'system-ui, -apple-system, sans-serif',
    }}>
      <CSVImporter
        columns={columns}
        onComplete={handleComplete}
        onError={handleError}
        onValidationError={handleValidationError}
        theme={{
          primaryColor: primaryColor,
        }}
      />
    </div>
  );
};

Troubleshooting

Component not appearing in library

Ensure you've run npx retool-ccl deploy and the deployment completed without errors. Check that your API token has the correct scopes: Custom Component Libraries read and write.

Event handler not firing

Verify the event callback is registered with the correct name:

const onImportComplete = Retool.useEventCallback({
  name: 'importComplete',  // This exact name must match in Retool
});

In your Retool app, check that the event handler is attached to importComplete, not a different event.

Data type mismatches on insert

If your database insert fails with type errors, ensure your column schema specifies the correct types:

[
  { "name": "amount", "label": "Amount", "required": true, "type": "number" },
  { "name": "is_active", "label": "Active", "required": true, "type": "boolean" }
]

ImportCSV will coerce values to the specified type before passing to Retool.

CORS errors in development

The development server may show CORS warnings in the browser console. These typically don't affect functionality. If you see actual blocked requests, ensure your Retool instance URL is correct in the login step.

Component exceeds size limit

If deployment fails with a size error, check your dependencies. The limit is 10MB per library revision. Avoid importing large libraries. If needed, use dynamic imports to split your bundle.

Feature comparison

FeatureNative RetoolCustom Component with ImportCSV
Setup timeMinutes~30 minutes
File size limit3MBNo practical limit
Column mappingManualVisual UI
Data validationBuild yourselfBuilt-in
Error previewNoneRow-by-row feedback
Type coercionLimitedAutomatic
Mobile supportYesNo (custom components not supported)

When to use each approach

Use native Retool file inputs when:

  • Files are under 3MB
  • CSV columns match your database schema exactly
  • You have technical users who can handle manual mapping
  • You need mobile app support

Use a custom component with ImportCSV when:

  • Files may exceed 3MB
  • Non-technical users need guided column mapping
  • Data validation and error feedback matter
  • You want to reduce support tickets from import errors

Next steps

If ImportCSV fits your needs, you can try it free with no credit card required.

Wrap-up

CSV imports shouldn't slow you down. ImportCSV aims to expand into your workflow — whether you're building data import flows, handling customer uploads, or processing large datasets.

If that sounds like the kind of tooling you want to use, try ImportCSV .