These five packages address CSV file handling and file upload workflows in React applications, but they serve different purposes. papaparse is a powerful vanilla JavaScript CSV parser that works in both browser and Node.js environments. react-csv provides React components specifically for exporting data as CSV files. react-csv-reader offers a React component for reading and parsing uploaded CSV files. react-dropzone is a general-purpose file dropzone component that handles any file type with drag-and-drop support. react-papaparse wraps papaparse functionality in React hooks and components for easier integration.
When building React applications that handle CSV files, you have several options — but they solve different problems. Some packages focus on parsing CSV data, others on exporting it, and some handle the file upload experience itself. Let's compare how these five packages tackle common CSV workflows.
papaparse is the foundation — a vanilla JavaScript library that parses CSV strings and files with extensive configuration.
import Papa from 'papaparse';
Papa.parse(file, {
header: true,
complete: (results) => {
console.log(results.data);
}
});
react-papaparse wraps papaparse in React hooks for easier state management within components.
import { usePapaParse } from 'react-papaparse';
function CsvUploader() {
const { readString } = usePapaParse();
const handleFile = (file) => {
readString(file, {
header: true,
complete: (results) => {
console.log(results.data);
}
});
};
}
react-csv-reader provides a pre-built component that handles both file selection and parsing.
import CsvReader from 'react-csv-reader';
function CsvUploader() {
const handleFileRead = (data) => {
console.log(data);
};
return <CsvReader onFileLoaded={handleFileRead} />;
}
react-dropzone handles file selection but requires you to add parsing logic separately.
import { useDropzone } from 'react-dropzone';
import Papa from 'papaparse';
function CsvUploader() {
const onDrop = (acceptedFiles) => {
acceptedFiles.forEach(file => {
Papa.parse(file, {
complete: (results) => console.log(results.data)
});
});
};
const { getRootProps, getInputProps } = useDropzone({ onDrop });
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
<p>Drop CSV files here</p>
</div>
);
}
react-csv does not support parsing — it only exports data as CSV downloads.
// react-csv has no parsing functionality
// It only provides CSVLink and CSVDownload components for export
react-csv specializes in exporting React data as downloadable CSV files with minimal code.
import { CSVLink } from 'react-csv';
function ExportButton({ data }) {
return (
<CSVLink data={data} filename="export.csv">
Download CSV
</CSVLink>
);
}
papaparse can write CSV strings but requires manual download handling.
import Papa from 'papaparse';
function exportData(data) {
const csv = Papa.unparse(data);
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
// Create and trigger download link manually
}
react-papaparse includes CSV writing through its hook but still needs manual download setup.
import { usePapaParse } from 'react-papaparse';
function ExportButton({ data }) {
const { unparse } = usePapaParse();
const handleExport = () => {
const csv = unparse(data);
// Manual download trigger needed
};
}
react-csv-reader and react-dropzone do not support CSV export — they focus on file input only.
// Neither react-csv-reader nor react-dropzone provide export functionality
// You would need to combine them with papaparse or react-csv for exports
react-dropzone gives you full control over the upload UI with render props and hooks.
import { useDropzone } from 'react-dropzone';
function CustomDropzone() {
const { getRootProps, getInputProps, isDragActive } = useDropzone();
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isDragActive ? (
<p>Drop the files here ...</p>
) : (
<p>Drag and drop CSV files here</p>
)}
</div>
);
}
react-csv-reader provides a styled component with limited customization options.
import CsvReader from 'react-csv-reader';
function SimpleUploader() {
return (
<CsvReader
onFileLoaded={handleData}
inputStyle={{ backgroundColor: '#f0f0f0' }}
/>
);
}
papaparse has no UI — you build the entire file input experience yourself.
import Papa from 'papaparse';
function ManualUploader() {
const handleFile = (e) => {
const file = e.target.files[0];
Papa.parse(file, { complete: (results) => {} });
};
return <input type="file" onChange={handleFile} />;
}
react-papaparse provides some pre-built components but encourages custom UI with hooks.
import { usePapaParse } from 'react-papaparse';
function HookBasedUploader() {
const { readString } = usePapaParse();
return (
<input
type="file"
onChange={(e) => readString(e.target.files[0], {})}
/>
);
}
react-csv provides styled link components for exports with basic customization.
import { CSVLink } from 'react-csv';
function StyledExport() {
return (
<CSVLink
data={data}
filename="export.csv"
className="btn btn-primary"
>
Export
</CSVLink>
);
}
papaparse supports streaming for large files to avoid memory issues.
import Papa from 'papaparse';
Papa.parse(file, {
chunk: (results) => {
// Process each chunk separately
console.log(results.data.length);
},
step: (row) => {
// Process row by row
},
worker: true // Use web worker for heavy parsing
});
react-papaparse inherits papaparse's streaming capabilities through its hook.
import { usePapaParse } from 'react-papaparse';
function LargeFileHandler() {
const { readString } = usePapaParse();
const handleLargeFile = (file) => {
readString(file, {
chunk: (results) => {
// Process chunks in component
},
worker: true
});
};
}
react-csv-reader, react-dropzone, and react-csv do not have built-in streaming support — they load files into memory.
// These packages load entire files into memory
// For large files, you would need to integrate papaparse directly
papaparse offers the most configuration options for parsing behavior.
import Papa from 'papaparse';
Papa.parse(file, {
delimiter: ',',
newline: '\n',
quoteChar: '"',
escapeChar: '"',
header: true,
transformHeader: (h) => h.toLowerCase(),
dynamicTyping: true,
skipEmptyLines: true,
encoding: 'utf-8'
});
react-papaparse passes the same configuration options through its hook.
import { usePapaParse } from 'react-papaparse';
function ConfigurableParser() {
const { readString } = usePapaParse();
const parseWithConfig = (file) => {
readString(file, {
header: true,
dynamicTyping: true,
skipEmptyLines: true
});
};
}
react-csv-reader has limited configuration focused on basic parsing needs.
import CsvReader from 'react-csv-reader';
function BasicReader() {
return (
<CsvReader
onFileLoaded={handleData}
parserOptions={{ delimiter: ',' }}
/>
);
}
react-csv focuses on export configuration rather than parsing.
import { CSVLink } from 'react-csv';
function ConfiguredExport() {
return (
<CSVLink
data={data}
headers={headers}
filename="export.csv"
enclosingCharacter={'"'}
/>
);
}
react-dropzone configures file acceptance rules rather than CSV parsing.
import { useDropzone } from 'react-dropzone';
function FileAcceptor() {
const { getRootProps, getInputProps } = useDropzone({
accept: {
'text/csv': ['.csv'],
'application/vnd.ms-excel': ['.xls']
},
maxFiles: 1,
maxSize: 10485760 // 10MB
});
}
Real-world applications often need multiple packages working together:
Use react-dropzone for flexible uploads + papaparse for parsing + react-csv for exports.
import { useDropzone } from 'react-dropzone';
import Papa from 'papaparse';
import { CSVLink } from 'react-csv';
function Dashboard() {
const [data, setData] = useState([]);
const onDrop = (files) => {
Papa.parse(files[0], {
header: true,
complete: (results) => setData(results.data)
});
};
const { getRootProps } = useDropzone({ onDrop, accept: {'text/csv': ['.csv']} });
return (
<div>
<div {...getRootProps()}>Drop CSV here</div>
<CSVLink data={data} filename="processed.csv">Export</CSVLink>
</div>
);
}
Use react-csv-reader for quick setup with minimal code.
import CsvReader from 'react-csv-reader';
function SimpleImport() {
const handleData = (data) => {
// Process imported data
};
return <CsvReader onFileLoaded={handleData} />;
}
Use react-papaparse for hook-based state management throughout your app.
import { usePapaParse } from 'react-papaparse';
function CsvWorkflow() {
const { readString, unparse } = usePapaParse();
const [parsedData, setParsedData] = useState([]);
const handleUpload = (file) => {
readString(file, {
header: true,
complete: (results) => setParsedData(results.data)
});
};
const handleExport = () => {
const csv = unparse(parsedData);
// Trigger download
};
}
As of current documentation:
papaparse — Actively maintained, stable API, widely used in productionreact-csv — Maintained but updates are infrequent; still functional for export needsreact-csv-reader — Limited recent activity; consider for simple use cases but evaluate alternatives for critical featuresreact-dropzone — Actively maintained with regular updates and strong community supportreact-papaparse — Maintained but depends on papaparse updates; verify compatibility with latest papaparse versions💡 Tip: For new projects requiring CSV parsing, start with
papaparsedirectly. Add React wrappers only if they simplify your specific workflow.
| Package | Primary Use | React-Specific | Parsing | Export | Custom UI |
|---|---|---|---|---|---|
papaparse | CSV parsing | ❌ | ✅ | ✅ (manual) | ❌ |
react-csv | CSV export | ✅ | ❌ | ✅ | Basic |
react-csv-reader | CSV import | ✅ | ✅ | ❌ | Limited |
react-dropzone | File upload | ✅ | ❌ (add separately) | ❌ | Full |
react-papaparse | CSV parsing | ✅ | ✅ | ✅ (manual) | Moderate |
Think about your actual workflow needs:
papaparse for maximum control, or react-papaparse for React hook integrationreact-csv for simple React component exportsreact-csv-reader for minimal setupreact-dropzone and add parsing separatelyreact-dropzone + papaparse + react-csvThe key insight: These packages solve different parts of the CSV workflow. Don't expect one package to do everything. Choose based on whether you need parsing, exporting, or file upload UI — then combine them as needed for your specific use case.
Choose react-dropzone if you need flexible drag-and-drop file upload functionality with extensive customization options. It handles any file type and provides hooks for building custom upload UIs. Ideal when you need full control over the upload experience or when CSV is just one of many file types your app accepts.
Choose react-csv if your primary need is exporting data from React applications as downloadable CSV files. It provides simple components like CSVLink and CSVDownload that integrate directly with your React component tree. Ideal for admin dashboards, reports, or any feature where users need to export table data.
Choose react-papaparse if you want papaparse's parsing power wrapped in React hooks and components for easier state management. It simplifies integration by providing usePapaParse hook and pre-built components. Best for React projects that need CSV parsing without manually wiring papaparse into your component lifecycle.
Choose react-csv-reader if you need a ready-made React component for users to upload and parse CSV files with minimal setup. It combines file input handling with CSV parsing in a single component. Best for simple upload forms where you don't need custom dropzone styling or advanced file handling features.
Choose papaparse if you need a robust, framework-agnostic CSV parser with extensive configuration options for parsing, writing, and streaming large files. It works in both browser and Node.js environments and gives you full control over parsing behavior. Best for projects that need CSV processing without React-specific bindings or when you want to build custom upload components.
Simple React hook to create a HTML5-compliant drag'n'drop zone for files.
Documentation and examples at https://react-dropzone.js.org. Source code at https://github.com/react-dropzone/react-dropzone/.
Install it from npm. react-dropzone ships as ESM and CommonJS with TypeScript types included, and works with any modern bundler (Vite, webpack, Rspack, etc.).
npm install react-dropzone
or:
yarn add react-dropzone
You can either use the hook:
import React, {useCallback} from "react";
import {useDropzone} from "react-dropzone";
function MyDropzone() {
const onDrop = useCallback(acceptedFiles => {
// Do something with the files
}, []);
const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop});
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
{isDragActive ? <p>Drop the files here ...</p> : <p>Drag 'n' drop some files here, or click to select files</p>}
</div>
);
}
Or the wrapper component for the hook:
import React from "react";
import Dropzone from "react-dropzone";
<Dropzone onDrop={acceptedFiles => console.log(acceptedFiles)}>
{({getRootProps, getInputProps}) => (
<section>
<div {...getRootProps()}>
<input {...getInputProps()} />
<p>Drag 'n' drop some files here, or click to select files</p>
</div>
</section>
)}
</Dropzone>;
If you want to access file contents you have to use the FileReader API:
import React, {useCallback} from "react";
import {useDropzone} from "react-dropzone";
function MyDropzone() {
const onDrop = useCallback(acceptedFiles => {
acceptedFiles.forEach(file => {
const reader = new FileReader();
reader.onabort = () => console.log("file reading was aborted");
reader.onerror = () => console.log("file reading has failed");
reader.onload = () => {
// Do whatever you want with the file contents
const binaryStr = reader.result;
console.log(binaryStr);
};
reader.readAsArrayBuffer(file);
});
}, []);
const {getRootProps, getInputProps} = useDropzone({onDrop});
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
<p>Drag 'n' drop some files here, or click to select files</p>
</div>
);
}
The dropzone property getters are just two functions that return objects with properties which you need to use to create the drag 'n' drop zone.
The root properties can be applied to whatever element you want, whereas the input properties must be applied to an <input>:
import React from "react";
import {useDropzone} from "react-dropzone";
function MyDropzone() {
const {getRootProps, getInputProps} = useDropzone();
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
<p>Drag 'n' drop some files here, or click to select files</p>
</div>
);
}
Note that whatever other props you want to add to the element where the props from getRootProps() are set, you should always pass them through that function rather than applying them on the element itself.
This is in order to avoid your props being overridden (or overriding the props returned by getRootProps()):
<div
{...getRootProps({
onClick: event => console.log(event),
role: "button",
"aria-label": "drag and drop area"
// ...and any other props
})}
/>
In the example above, the provided {onClick} handler will be invoked before the internal one, therefore, internal callbacks can be prevented by simply using stopPropagation.
See Events for more examples.
Important: if you omit rendering an <input> and/or binding the props from getInputProps(), opening a file dialog will not be possible.
Both getRootProps and getInputProps accept a custom refKey (defaults to ref) as one of the attributes passed down in the parameter.
This can be useful when the element you're trying to apply the props from either one of those fns does not expose a reference to the element, e.g:
import React from "react";
import {useDropzone} from "react-dropzone";
// NOTE: After v4.0.0, styled components exposes a ref using forwardRef,
// therefore, no need for using innerRef as refKey
import styled from "styled-components";
const StyledDiv = styled.div`
// Some styling here
`;
function Example() {
const {getRootProps, getInputProps} = useDropzone();
return (
<StyledDiv {...getRootProps({refKey: "innerRef"})}>
<input {...getInputProps()} />
<p>Drag 'n' drop some files here, or click to select files</p>
</StyledDiv>
);
}
If you're working with Material UI v4 and would like to apply the root props on some component that does not expose a ref, use RootRef:
import React from "react";
import {useDropzone} from "react-dropzone";
import RootRef from "@material-ui/core/RootRef";
function PaperDropzone() {
const {getRootProps, getInputProps} = useDropzone();
const {ref, ...rootProps} = getRootProps();
return (
<RootRef rootRef={ref}>
<Paper {...rootProps}>
<input {...getInputProps()} />
<p>Drag 'n' drop some files here, or click to select files</p>
</Paper>
</RootRef>
);
}
IMPORTANT: do not set the ref prop on the elements where getRootProps()/getInputProps() props are set, instead, get the refs from the hook itself:
import React from "react";
import {useDropzone} from "react-dropzone";
function Refs() {
const {
getRootProps,
getInputProps,
rootRef, // Ref to the `<div>`
inputRef // Ref to the `<input>`
} = useDropzone();
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
<p>Drag 'n' drop some files here, or click to select files</p>
</div>
);
}
If you're using the <Dropzone> component, though, you can set the ref prop on the component itself which will expose the {open} prop that can be used to open the file dialog programmatically:
import React, {createRef} from "react";
import Dropzone from "react-dropzone";
const dropzoneRef = createRef();
<Dropzone ref={dropzoneRef}>
{({getRootProps, getInputProps}) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
<p>Drag 'n' drop some files here, or click to select files</p>
</div>
)}
</Dropzone>;
dropzoneRef.open();
react-dropzone makes some of its drag 'n' drop callbacks asynchronous to enable promise based getFilesFromEvent() functions. In order to test components that use this library, you need to use the react-testing-library:
import React from "react";
import Dropzone from "react-dropzone";
import {act, fireEvent, render} from "@testing-library/react";
test("invoke onDragEnter when dragenter event occurs", async () => {
const file = new File([JSON.stringify({ping: true})], "ping.json", {type: "application/json"});
const data = mockData([file]);
const onDragEnter = jest.fn();
const ui = (
<Dropzone onDragEnter={onDragEnter}>
{({getRootProps, getInputProps}) => (
<div {...getRootProps()}>
<input {...getInputProps()} />
</div>
)}
</Dropzone>
);
const {container} = render(ui);
await act(() => fireEvent.dragEnter(container.querySelector("div"), data));
expect(onDragEnter).toHaveBeenCalled();
});
function mockData(files) {
return {
dataTransfer: {
files,
items: files.map(file => ({
kind: "file",
type: file.type,
getAsFile: () => file
})),
types: ["Files"]
}
};
}
NOTE: using Enzyme for testing is not supported at the moment, see #2011.
More examples for this can be found in react-dropzone's own test suites.
React 18 or above is required because we use hooks (the lib itself is a hook).
Files returned by the hook or passed as arg to the onDrop cb won't have the properties path or fullPath.
For more inf check this SO question and this issue.
Inside a Tauri webview, OS file drops are intercepted by the native window layer before the webview sees them, so the browser's drag 'n' drop events never fire and the dropzone appears to ignore drops. Disable Tauri's native handling to let react-dropzone work unchanged: set dragDropEnabled: false (Tauri v2) or fileDropEnabled: false (Tauri v1) in tauri.conf.json. If you instead need the absolute paths of dropped files, keep native handling on and drive your own state from Tauri's drag-drop event. See the Using with Tauri guide and #1316.
This lib is not a file uploader; as such, it does not process files or provide any way to make HTTP requests to some server; if you're looking for that, checkout filepond or uppy.io.
Pasting files into the dropzone is supported out of the box: when the dropzone (or a focused child, e.g. a <textarea>) has focus and the user pastes files with Ctrl/Cmd+V - for instance a screenshot copied to the clipboard - those files run through the same accept, size and validator checks and the same onDrop/onDropAccepted/onDropRejected callbacks as a drop. Pastes that carry no files (plain text and the like) are ignored and left untouched, so pasting into inputs keeps working.
The paste is only received when the dropzone has focus, so pair it with autoFocus or let the user click/tab into it first. To disable paste handling, set noPaste:
import React from "react";
import {useDropzone} from "react-dropzone";
function MyDropzone() {
const {getRootProps, getInputProps} = useDropzone({noPaste: true});
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
<p>Drag 'n' drop or click - pasting is disabled</p>
</div>
);
}
If you use <label> as the root element, the file dialog will be opened twice; see #1107 and #1432 why. A <label> natively forwards clicks to the <input> it wraps, so the dialog opens once from that and once from our own click handler. To avoid this, use noClick:
import React, {useCallback} from "react";
import {useDropzone} from "react-dropzone";
function MyDropzone() {
const {getRootProps, getInputProps} = useDropzone({noClick: true});
return (
<label {...getRootProps()}>
<input {...getInputProps()} />
</label>
);
}
If you bind a click event on an inner element and use open(), it will trigger a click on the root element too, resulting in the file dialog opening twice. To prevent this, use the noClick on the root:
import React, {useCallback} from "react";
import {useDropzone} from "react-dropzone";
function MyDropzone() {
const {getRootProps, getInputProps, open} = useDropzone({noClick: true});
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
<button type="button" onClick={open}>
Open
</button>
</div>
);
}
Browsers only let you open the file dialog from within a short window after a user gesture (a "transient user activation"). Because of this, open() must run synchronously in the same event that the user triggered - it cannot wait for React to re-render.
So if you update a prop (e.g. accept) and call open() in the same handler, open() runs against the previous render, before the new prop is applied. Some browsers (notably Safari) may also drop the file dialog entirely, so it appears to require a second click. See #1188.
// 🚫 open() runs with the old `accept` - and may not open at all on Safari
function MyDropzone() {
const [accept, setAccept] = useState({"image/*": []});
const {getRootProps, getInputProps, open} = useDropzone({accept, noClick: true});
const pickPdfs = () => {
setAccept({"application/pdf": []}); // applied on the next render
open(); // still uses {"image/*": []}
};
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
<button type="button" onClick={pickPdfs}>
Pick PDFs
</button>
</div>
);
}
Instead, render one dropzone per set of props (each with its own open) and call the right one, so no state change has to land before opening:
function MyDropzone() {
const images = useDropzone({accept: {"image/*": []}, noClick: true});
const pdfs = useDropzone({accept: {"application/pdf": []}, noClick: true});
return (
<div {...images.getRootProps()}>
<input {...images.getInputProps()} />
<input {...pdfs.getInputProps()} />
<button type="button" onClick={images.open}>
Pick images
</button>
<button type="button" onClick={pdfs.open}>
Pick PDFs
</button>
</div>
);
}
On Android 13+, Chrome and Edge open the native Android photo picker whenever a file input's accept attribute resolves to only image and/or video types (e.g. accept={{"image/*": []}}). That picker only lists the gallery and files - it has no "take a photo"/camera action - so the camera appears to be missing. See #1417.
This is browser/OS behavior, not something the lib controls, and it's unrelated to how the <input> is styled: a plain <input type="file"> with no accept (or one that also allows a non-media type) falls back to the older chooser that does include the Camera app.
If you need the camera, use the capture attribute, which you can pass straight through getInputProps():
// Camera only (no gallery/files)
<input {...getInputProps({capture: "environment"})} />
To offer both the camera and the gallery, render one dropzone per input (each with its own open) and let the user pick which one to trigger:
import React from "react";
import {useDropzone} from "react-dropzone";
function MyDropzone({onDrop}) {
const gallery = useDropzone({accept: {"image/*": []}, noClick: true, onDrop});
const camera = useDropzone({accept: {"image/*": []}, noClick: true, onDrop});
return (
<>
<button type="button" onClick={gallery.open}>
Choose from gallery
</button>
<input {...gallery.getInputProps()} />
<button type="button" onClick={camera.open}>
Take a photo
</button>
<input {...camera.getInputProps({capture: "environment"})} />
</>
);
}
The onFileDialogCancel() cb is unstable in most browsers, meaning, there's a good chance of it being triggered even though you have selected files.
We rely on using a timeout of 300ms after the window is focused (the window onfocus event is triggered when the file select dialog is closed) to check if any files were selected and trigger onFileDialogCancel if none were selected.
As one can imagine, this doesn't really work if there's a lot of files or large files as by the time we trigger the check, the browser is still processing the files and no onchange events are triggered yet on the input. Check #1031 for more info.
Fortunately, there's the File System Access API, which is currently a working draft and some browsers support it (see browser compatibility), that provides a reliable way to prompt the user for file selection and capture cancellation.
Also keep in mind that the FS access API can only be used in secure contexts.
NOTE You can enable using the FS access API with the useFsAccessApi property: useDropzone({useFsAccessApi: true}).
When setting useFsAccessApi to true, you're switching to the File System API (see the file system access RFC).
What this essentially does is that it will use the showOpenFilePicker method to open the file picker window so that the user can select files.
In contrast, the traditional way (when the useFsAccessApi is not set to true or not specified) uses an <input type="file"> (see docs) on which a click event is triggered.
With the use of the file system access API enabled, there's a couple of caveats to keep in mind:
showOpenFilePicker() and reject with a NotAllowedError (e.g. Microsoft Edge for Business or other restrictive enterprise/security policies). When this happens we automatically fall back to the native <input> so the file dialog still opens, provided you render one via getInputProps() (see #1429)We use browserslist config to state the browser support for this lib, so check it out on browserslist.dev.
React Dropzone integrates perfectly with Pintura Image Editor, creating a modern image editing experience. Pintura supports crop aspect ratios, resizing, rotating, cropping, annotating, filtering, and much more.
Checkout the Pintura integration example.
Support us with a monthly donation and help us continue our activities. [Become a backer]
Become a sponsor and get your logo on our README on Github with a link to your site. [Become a sponsor]
react-dropzone.js.org hosting provided by netlify.
Checkout the organization CONTRIBUTING.md.
MIT