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.

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:
- Drag a File Button component onto your canvas
- In the Inspector panel, enable Parse files
- 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:
- Create a new query for your database
- Set the action type to Bulk insert
- 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:
| Limitation | Details |
|---|---|
| File size limit | 3MB for CSV import tool |
| No column mapping UI | Manual mapping required |
| No data validation | Must build custom validation |
| No error preview | Errors appear on insert |
| Null value handling | Issues with nullable columns |
| Numeric parsing | Boolean 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:
- Your React component wraps ImportCSV and handles file selection
- ImportCSV provides column mapping UI and validation
- Parsed data passes to Retool via state variables
- Event callbacks notify your Retool app when import completes
- 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 installLog in to your Retool instance:
npx retool-ccl loginThis 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 initFollow the prompts to name your library.
Step 2: Install ImportCSV
Add ImportCSV to your component dependencies:
npm install @importcsv/reactStep 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 objectscsvImporter.importStatus- Object withsuccess,rowCount, anderrorfieldscsvImporter.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 devThis starts a local server and provides instructions for adding the development component to a Retool app:
- Open your Retool app in edit mode
- Open the Component Library panel
- Click "Add custom component"
- 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 deployThis 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:
- Open the Component Library panel
- Find your CSV Importer component
- Drag it onto the canvas
- 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:
- Select the CSV Importer component
- In the Inspector, find Event handlers
- Add a handler for
importComplete - 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 itemFor bulk insert in GUI mode:
- Create a new query
- Select Bulk insert action
- 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
| Feature | Native Retool | Custom Component with ImportCSV |
|---|---|---|
| Setup time | Minutes | ~30 minutes |
| File size limit | 3MB | No practical limit |
| Column mapping | Manual | Visual UI |
| Data validation | Build yourself | Built-in |
| Error preview | None | Row-by-row feedback |
| Type coercion | Limited | Automatic |
| Mobile support | Yes | No (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
- Review the Retool custom component documentation
- Explore the TypeScript API reference
- Check out ImportCSV's validation and transformation options in our docs
If ImportCSV fits your needs, you can try it free with no credit card required.
Related posts
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 .