Skip to main content

Export

Export the grid to CSV or Excel — from the toolbar menu or imperatively via apiRef. One descriptor (the ExportRequest) decides which columns, which rows, and what formatting an export covers; an export mode decides where the file is built. The default mode (client) needs no backend at all. For very large datasets (hundreds of thousands → millions of rows), switch to a server mode so the browser never has to hold or build the whole file.

Loading demo…

Open the export icon in the toolbar and choose CSV or Excel — a file downloads.

<DataTable columns={columns} data={data} enableExport exportFilename="users" />

// …or imperatively
apiRef.current?.export.exportCSV();
apiRef.current?.export.exportExcel();
apiRef.current?.export.exportCSV({ mode: 'server-file', onlySelectedRows: true });

The four export modes

Set exportMode (default 'client'), or override per call with exportCSV({ mode }).

ModeWho fetches rowsWho formats columnsWho builds the fileClient memoryBackend needed
client (default)client (in-memory, or pages onFetchData)clientclient (streams to disk where supported)boundednone
server-dataserver streams rows (onExportStream)clientclient (stream-writes)boundedstreaming endpoint
server-fileserverserverserver → { blob } / { fileUrl }~0export endpoint
server-asyncserver (job)serverserver job → { fileUrl }~0job + storage

Rule of thumb: client up to ~100k rows; CSV + a server mode above that. Excel (XLSX) is capped by the format itself at 1,048,576 rows per sheet — see Limits.

How columns, rows & formatting flow — the ExportRequest

For every export the grid builds one descriptor from live state, so behaviour is identical no matter where the file is built:

interface ExportRequest {
format: 'csv' | 'excel';
filename: string;
columns: { id: string; header: string; format?: ValueFormat }[]; // visible, non-hideInExport, in display order
scope: 'all' | 'filtered' | 'selected';
selection?: SelectionState; // include/exclude ids (when scope === 'selected')
filters?: unknown; // current global + column filters
sorting?: unknown; // current sort
includeHeaders?: boolean;
delimiter?: string;
sanitizeCSV?: boolean;
}
  • client / server-data keep formatting on the client — the server only supplies raw rows, the grid applies your column transforms. The server receives just filters / sorting / selection.
  • server-file / server-async send the whole ExportRequest (including columns + headers + format hints) so the server can produce exactly the visible columns, formatted, in order.

Choosing what to export

Pass options to exportCSV / exportExcel (or set table-level defaults):

OptionTypeDefaultEffect
onlyVisibleColumnsbooleantrueExport visible columns, else all leaf columns.
columnsstring[]Explicit column-id whitelist, in the given order.
scope'all' | 'filtered' | 'selected'derivedForce a scope; otherwise inferred from selection + active filters.
onlySelectedRowsbooleanfalseShortcut: export the current selection.
includeHeadersbooleantrueWrite a header row.
delimiterstring,CSV delimiter.
// Export only the selected rows, only the Name + Email columns:
apiRef.current?.export.exportCSV({
onlySelectedRows: true,
columns: ['name', 'email'],
filename: 'selected-contacts',
});

Per-column export options

Set these on any ColumnDef — they apply across all modes. For client / server-data, exportValue / exportFormat functions run client-side; for server-file / server-async the server formats, using the header + format hint carried in the request.

OptionTypeDescription
hideInExportbooleanExclude the column from every export.
exportHeaderstring | (ctx) => stringOverride the column's export header.
exportValue(ctx) => anyTransform the raw value before formatting.
exportFormat'auto' | 'string' | 'number' | 'boolean' | 'json' | 'date' | (ctx) => anyCoerce/format the value.
const columns = [
{ id: 'name', header: 'Name', accessorKey: 'name' },
{ id: 'createdAt', header: 'Created', accessorKey: 'createdAt',
exportHeader: 'Created (ISO)', exportFormat: 'date' },
{ id: 'actions', header: '', cell: ActionsCell, hideInExport: true },
];

Mode 1 — client (default)

The browser fetches the rows (already in memory for client data, or pages your onFetchData for server data), formats them with your column defs, and writes the file. CSV is written incrementally:

  • Chromium (secure context): with exportSink: 'auto' (default) the file streams straight to disk via the File System Access API — near-constant memory. The save dialog opens on click.
  • Everywhere else: falls back to an in-memory Blob (bounded by total file size — fine to hundreds of MB). Set exportSink: 'blob' to always use this path (silent auto-download, no save dialog).

Formatting runs on the main thread with cooperative yields (the UI keeps responding) and progress is throttled to every exportProgressEvery rows — not per row. There is no Web Worker by default, because exportValue / exportFormat can be functions, which can't cross a Worker boundary. For genuinely huge exports, prefer a server mode.

<DataTable
columns={columns}
data={data}
enableExport
exportMode="client" // default
exportSink="auto" // 'auto' | 'stream' | 'blob'
exportInterPageDelayMs={0} // server data: delay between pages (raise for rate limits)
exportFetchConcurrency={6} // server data: pages in flight at once
/>

Paging server data in client mode: the grid reuses onFetchData to page through everything (without disturbing the visible page). The old hard-coded 100 ms inter-page delay is gone — it defaults to 0 and is configurable via exportInterPageDelayMs.


Mode 2 — server-data (server streams rows, client formats)

The server streams the raw filtered/sorted rows; the client formats them and stream-writes the file. Provide onExportStream, returning an AsyncIterable<TData[]> (batches of rows):

<DataTable
columns={columns}
dataMode="server"
enableExport
exportMode="server-data"
onExportStream={async function* (request, signal) {
const res = await fetch('/api/users/export-stream', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ filters: request.filters, sorting: request.sorting, selection: request.selection }),
signal,
});
// Server responds with NDJSON (one JSON row per line), flushed as it reads the DB cursor.
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buf = '';
let batch: User[] = [];
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
let nl: number;
while ((nl = buf.indexOf('\n')) >= 0) {
const line = buf.slice(0, nl); buf = buf.slice(nl + 1);
if (line.trim()) batch.push(JSON.parse(line));
if (batch.length >= 1000) { yield batch; batch = []; }
}
}
if (batch.length) yield batch;
}}
/>
// Server (Node/Express) — stream a keyset cursor as NDJSON, O(1) memory:
app.post('/api/users/export-stream', async (req, res) => {
res.setHeader('content-type', 'application/x-ndjson');
for await (const row of streamUsersByKeyset(req.body)) res.write(JSON.stringify(row) + '\n');
res.end();
});

Mode 3 — server-file (server builds the file)

The server formats and builds the finished file from the full ExportRequest (so it knows the visible columns, headers, order, selection, filters, and sort). Provide onServerExport, returning { blob } or { fileUrl }:

<DataTable
columns={columns}
dataMode="server"
enableExport
exportMode="server-file"
onServerExport={async (request, signal) => {
const res = await fetch('/api/users/export', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(request), // columns + headers + scope + selection + filters + sort + format
signal,
});
const { url } = await res.json();
return { fileUrl: url }; // or: return { blob: await res.blob() }
}}
/>

{ fileUrl } is downloaded via native <a download> navigation — the browser's download manager streams it to disk outside JS (O(1) memory). Two common server shapes:

  • Stream the response — the endpoint sets Content-Disposition: attachment and streams the CSV with chunked transfer encoding; return its URL.
  • Generate to storage — write the file to S3/GCS, return a short-lived presigned URL.

The lib does not fetch().blob() a fileUrl (that would buffer the whole file). It navigates to it. Set exportRenameDownload only if you must force the saved filename across origins (that path buffers — unsuitable for huge files).


Mode 4 — server-async (job + poll)

For exports that take minutes (millions of rows), no HTTP request should stay open. The server enqueues a job and returns { jobId }; the client polls until a { fileUrl } is ready. Provide onServerExport (returns the job) and onExportPoll (returns status):

<DataTable
columns={columns}
dataMode="server"
enableExport
exportMode="server-async"
exportPollIntervalMs={2000}
onServerExport={async (request) => {
const { jobId, statusUrl } = await api.post('/api/exports', request);
return { jobId, statusUrl };
}}
onExportPoll={async (job, signal) => {
const s = await api.get(job.statusUrl ?? `/api/exports/${job.jobId}`, { signal });
return {
status: s.state, // 'pending' | 'processing' | 'ready' | 'error'
fileUrl: s.url, // present when status === 'ready'
percentage: s.pct,
totalRows: s.total,
message: s.error,
};
}}
/>

The grid surfaces job progress through the normal lifecycle (waiting phase + percentage) and supports cancellation via apiRef.current.export.cancelExport().


Progress, lifecycle & cancellation

Every mode drives the same lifecycle. Subscribe via callbacks:

<DataTable
enableExport
onExportStateChange={(s) => setPhase(s.phase)} // see phases below
onExportProgress={({ processedRows, totalRows, percentage }) => setPct(percentage)}
onExportComplete={({ filename, totalRows }) => toast(`Saved ${filename} (${totalRows} rows)`)}
onExportError={({ message }) => toast.error(message)}
onExportCancel={() => toast('Export cancelled')}
/>
ExportPhaseMeaning
startingExport began.
fetchingPulling rows (client / server-data).
processingFormatting / server is building the file.
waitingAsync server job is running (server-async).
downloadingWriting / downloading the file.
completed / cancelled / errorTerminal states.
apiRef.current?.export.cancelExport(); // abort the running export
apiRef.current?.export.isExporting(); // boolean

exportConcurrency controls what happens if an export starts while one is running: 'cancelAndRestart' (default), 'queue', or 'ignoreIfRunning'.

Format policy & limits

  • Prefer CSV for large exports. CSV has no row limit and streams trivially.
  • Excel (XLSX) caps at 1,048,576 rows per sheet. Beyond it, exportExcel throws (set exportTruncateXlsx to cap-and-truncate instead). XLSX is built whole in memory (it can't stream line-by-line), and xlsx is lazy-loaded only when you export Excel — it's not in your initial bundle. Use a server mode for large Excel.
  • CSV-injection sanitization is on by default (exportSanitizeCSV) — leading =, +, -, @, TAB, and CR are neutralized so spreadsheets don't execute cell content as a formula.

Caveats

  • File System Access streaming is Chromium-only. Firefox/Safari fall back to the in-memory Blob path, which is bounded by device RAM — not a reliable 1M+ path. Use a server mode for guaranteed scale.
  • iOS Safari ignores the download attribute — client-generated blobs open inline. For a guaranteed save on iOS, deliver via a server URL with Content-Disposition: attachment (a server mode).
  • Validate fileUrl server-side. The lib only follows http(s) / blob URLs, but you should still ensure your endpoint returns trusted URLs (no open-redirect / SSRF).

Props reference

PropTypeDefaultDescription
enableExportbooleanfalseShow the toolbar export menu.
exportMode'client' | 'server-data' | 'server-file' | 'server-async''client'Where the file is built.
exportFilenamestring'export'Base filename.
exportSink'auto' | 'stream' | 'blob''auto'Client file sink.
exportChunkSizenumber1000Rows per page when paging server data.
exportInterPageDelayMsnumber0Delay between paged fetches.
exportFetchConcurrencynumber1Concurrent paged fetches (known total).
exportMaxClientRowsnumber1000000Guard for client-built files.
exportTruncateXlsxbooleanfalseTruncate XLSX at the row cap instead of throwing.
exportPollIntervalMsnumber2000Poll interval for server-async.
exportRenameDownloadbooleanfalseForce a renamed (buffered) fileUrl download.
exportProgressEverynumber2000Emit progress at most every N rows.
exportConcurrency'cancelAndRestart' | 'queue' | 'ignoreIfRunning''cancelAndRestart'Concurrent-export policy.
exportSanitizeCSVbooleantrueNeutralize CSV formula injection.
onServerExport(request, signal) => Promiseserver-file / server-async builder.
onExportStream(request, signal) => AsyncIterableserver-data row source.
onExportPoll(job, signal) => Promiseserver-async poll.
onExportProgress / onExportComplete / onExportError / onExportStateChange / onExportCancelcallbacksLifecycle.

API

// options: { mode, scope, columns, onlyVisibleColumns, onlySelectedRows, filename, delimiter, includeHeaders, chunkSize, sanitizeCSV }
apiRef.current.export.exportCSV(options?);
apiRef.current.export.exportExcel(options?);
apiRef.current.export.isExporting(); // boolean
apiRef.current.export.cancelExport();

See Server-side data for the onFetchData contract that client and server-data modes reuse for paging.