Data Export (Export CSV, Excel & Data)
Export displayed data, checked rows, or full source data to Excel (.xlsx) or CSV without external dependencies, or extract a logical model for external libraries.
import { t } from './i18n';
import * as React from 'react';
import { Button, Space, Tag } from 'antd';
import { Download, FileSpreadsheet } from 'lucide-react';
import {
BGrid,
type BGridColumn,
type BGridDataItem,
type BGridRef,
} from 'beautiful-grid';
import DataGridContainer from '../components/DataGridContainer';
import { useContainerSize } from '../hooks/useContainerSize';
interface Order {
orderNo: string;
customer: string;
product: string;
category: string;
price: number;
quantity: number;
status: string;
orderDate: string;
}
const customers = ['AxisJ Studio', 'Northwind', t('서울 물류', 'Seoul Logistics'), 'Mono Office', 'Global Tech'];
const products = [
t('클라우드 서버 패키지', 'Cloud Server Package'),
t('데이터베이스 라이선스', 'Database License'),
t('보안 컨설팅 서비스', 'Security Consulting'),
t('엔터프라이즈 모니터링', 'Enterprise Monitoring'),
t('네트워크 방화벽 장비', 'Network Firewall Unit'),
];
const categories = ['Infrastructure', 'Software', 'Security', 'DevOps', 'Hardware'];
const statuses = [t('결제 완료', 'Paid'), t('배송 중', 'Shipping'), t('처리 완료', 'Completed'), t('보류', 'Pending')];
const data: BGridDataItem<Order>[] = Array.from({ length: 40 }, (_, index) => {
const price = (index % 5 + 1) * 150000;
const quantity = (index % 4) + 1;
const month = String((index % 12) + 1).padStart(2, '0');
const day = String((index % 28) + 1).padStart(2, '0');
return {
values: {
orderNo: `ORD-2026-${String(index + 1).padStart(4, '0')}`,
customer: customers[index % customers.length],
product: products[index % products.length],
category: categories[index % categories.length],
price,
quantity,
status: statuses[index % statuses.length],
orderDate: `2026-${month}-${day}`,
},
};
});
const columns: BGridColumn<Order>[] = [
{
id: 'orderNo',
key: 'orderNo',
label: t('주문 번호', 'Order No'),
width: 140,
exportHeader: 'Order Number',
toolbox: true,
},
{
id: 'customer',
key: 'customer',
label: t('고객사', 'Customer'),
width: 140,
toolbox: true,
},
{
id: 'product',
key: 'product',
label: t('주문 품목', 'Product'),
width: 180,
toolbox: true,
},
{
id: 'category',
key: 'category',
label: t('분류', 'Category'),
width: 120,
toolbox: true,
},
{
id: 'price',
key: 'price',
label: t('단가', 'Unit Price'),
width: 120,
align: 'right',
toolbox: true,
itemRender: ({ value }) => `₩${Number(value).toLocaleString()}`,
getExportValue: ({ value }) => Number(value),
},
{
id: 'quantity',
key: 'quantity',
label: t('수량', 'Quantity'),
width: 80,
align: 'right',
toolbox: true,
},
{
id: 'status',
key: 'status',
label: t('상태', 'Status'),
width: 110,
align: 'center',
toolbox: true,
exportHeader: col => `${String(col.id).toUpperCase()} (STATUS)`,
},
{
id: 'orderDate',
key: 'orderDate',
label: t('주문 일자', 'Order Date'),
width: 120,
toolbox: true,
},
];
export default function ExportExample() {
const containerRef = React.useRef<HTMLDivElement>(null);
const { width, height } = useContainerSize(containerRef);
const gridRef = React.useRef<BGridRef<Order>>(null);
const [checkedRowKeys, setCheckedRowKeys] = React.useState<React.Key[]>([]);
const handleExportAllExcel = React.useCallback(() => {
gridRef.current?.exportExcel({
fileName: 'all-orders.xlsx',
rows: 'source',
columns: 'all',
});
}, []);
const handleExportAllCsv = React.useCallback(() => {
gridRef.current?.exportCsv({
fileName: 'all-orders.csv',
rows: 'source',
columns: 'all',
});
}, []);
const handleExportCheckedExcel = React.useCallback(() => {
gridRef.current?.exportExcel({
fileName: 'selected-orders.xlsx',
rows: 'checked',
columns: 'visible',
});
}, []);
const handleExportCheckedCsv = React.useCallback(() => {
gridRef.current?.exportCsv({
fileName: 'selected-orders.csv',
rows: 'checked',
columns: 'visible',
});
}, []);
return (
<div className='flex min-h-0 flex-col gap-3'>
<div className='flex flex-wrap items-center justify-between gap-3 rounded-lg border border-slate-200 bg-slate-50 p-3 text-sm text-slate-700'>
<Space wrap>
<Button icon={<FileSpreadsheet size={14} />} type='primary' onClick={handleExportAllExcel}>
{t('전체 로드 데이터 Excel', 'Export all loaded rows (Excel)')}
</Button>
<Button icon={<Download size={14} />} onClick={handleExportAllCsv}>
{t('전체 로드 데이터 CSV', 'Export all loaded rows (CSV)')}
</Button>
<Button
icon={<FileSpreadsheet size={14} />}
disabled={checkedRowKeys.length === 0}
onClick={handleExportCheckedExcel}
>
{t('선택 행 Excel', 'Export checked rows (Excel)')} ({checkedRowKeys.length})
</Button>
<Button
icon={<Download size={14} />}
disabled={checkedRowKeys.length === 0}
onClick={handleExportCheckedCsv}
>
{t('선택 행 CSV', 'Export checked rows (CSV)')} ({checkedRowKeys.length})
</Button>
</Space>
<div className='flex items-center gap-2'>
<Tag color='blue'>{t('우클릭 메뉴 지원', 'Context Menu Supported')}</Tag>
<span className='text-xs text-slate-500'>
{t('툴박스 필터·정렬 및 컬럼 표시 설정 반영', 'Reflects toolbox filters, sorting, and visibility')}
</span>
</div>
</div>
<DataGridContainer ref={containerRef} minHeight={480}>
<BGrid<Order>
ref={gridRef}
width={width}
height={height}
columns={columns}
data={data}
rowKey='orderNo'
rowChecked={{
checkedRowKeys,
onChange: (_indexes, keys) => setCheckedRowKeys(keys),
}}
columnVisibility={true}
contextMenuOptions={{
items: () => [
{
id: 'export-all-excel',
label: t('전체 로드 행 Excel 내보내기', 'Export all loaded rows to Excel'),
icon: <FileSpreadsheet size={14} />,
onSelect: () => handleExportAllExcel(),
},
{
id: 'export-all-csv',
label: t('전체 로드 행 CSV 내보내기', 'Export all loaded rows to CSV'),
icon: <Download size={14} />,
onSelect: () => handleExportAllCsv(),
},
{
type: 'separator',
id: 'export-sep',
},
{
id: 'export-checked-excel',
label: t('체크된 행 Excel 내보내기', 'Export checked rows to Excel'),
icon: <FileSpreadsheet size={14} />,
disabled: checkedRowKeys.length === 0,
onSelect: () => handleExportCheckedExcel(),
},
{
id: 'export-checked-csv',
label: t('체크된 행 CSV 내보내기', 'Export checked rows to CSV'),
icon: <Download size={14} />,
disabled: checkedRowKeys.length === 0,
onSelect: () => handleExportCheckedCsv(),
},
],
}}
/>
</DataGridContainer>
</div>
);
}import * as React from 'react';
import './DataGridContainer.css';
interface DataGridContainerProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
}
/**
* Keeps a DataGrid in a measured, fixed layout box.
*
* BGrid's rendered root is absolutely positioned within this relative
* container. This makes a ResizeObserver measurement authoritative when a
* surrounding flex or grid layout shrinks as well as when it expands.
*/
const DataGridContainer = React.forwardRef<HTMLDivElement, DataGridContainerProps>(
({ className, ...rest }, ref) => (
<div ref={ref} className={`data-grid-container ${className ?? ''}`.trim()} {...rest} />
),
);
DataGridContainer.displayName = 'DataGridContainer';
export default DataGridContainer;.data-grid-container {
position: relative;
width: 100%;
height: 400px;
overflow: hidden;
font-size: 13px;
}
.data-grid-container > .bgrid-root {
position: absolute;
inset: 0;
}import * as React from 'react';
export function useContainerSize(ref: React.MutableRefObject<HTMLElement | null>, additionalDeps: unknown[] = []) {
const [width, setWidth] = React.useState(0);
const [height, setHeight] = React.useState(0);
const resizeObserver = React.useRef(
new ResizeObserver(entries => {
if (entries.length !== 1) {
throw new Error('Invalid Container length');
}
const [entry] = entries;
const { width, height } = entry.contentRect;
setWidth(width);
setHeight(height);
}),
);
React.useEffect(() => {
if (!ref.current) return;
const observer = resizeObserver.current;
const element = ref.current;
setWidth(element.clientWidth);
setHeight(element.clientHeight);
observer.observe(element);
return () => {
observer.unobserve(element);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...additionalDeps, ref]);
return {
width,
height,
};
}1. Overview
BeautifulGrid provides a built-in export system without external dependencies (such as SheetJS or ExcelJS).
Because BeautifulGrid uses virtual scrolling, only visible rows are rendered in the DOM. Attempting to scrape DOM nodes causes data loss. BeautifulGrid exports data directly from its logical state model (store.data, store.sourceData, sourceIndexByVisibleIndex) ensuring reliable and complete exports regardless of scroll position.
gridRef.current.exportExcel(options): Generates standard OpenXML.xlsxfiles and triggers browser download with zero external dependenciesgridRef.current.exportCsv(options): Serializes data to CSV and triggers a browser file download (RFC 4180 compliant, UTF-8 BOM, formula injection prevention)gridRef.current.getExportData(options): Extracts logical column and cell 2D arrays (compatible with JSON, SheetJS, ExcelJS, etc.)
2. Row Scope
Specify the target rows using the rows option (default: 'displayed').
rows Option |
Target Data | Description |
|---|---|---|
'displayed' |
Visible logical rows | Current rows with filters, sorting, and tree folding applied. Preserves exact display order regardless of virtualization. |
'checked' |
Selected rows | Rows selected via rowChecked in sourceData order. Includes checked rows currently hidden by filters. |
'source' |
Loaded source rows | All rows currently loaded in the client-side sourceData. |
// Export current filtered and sorted rows
gridRef.current?.exportCsv({
fileName: 'orders.csv',
rows: 'displayed',
});
// Export only checked rows
gridRef.current?.exportCsv({
fileName: 'checked-orders.csv',
rows: 'checked',
});
[!WARNING] Server-side Pagination Notice
rows: 'source'refers to the rows currently loaded insourceDataon the client, not the entire remote database table. BeautifulGrid does not automatically fetch subsequent pages during export. For full database exports, use a server-side export endpoint.
3. Column Scope
Specify which columns to export using the columns option (default: 'visible').
'visible': Currently visible columns after column visibility projection'all': All columns including hidden columns (in pivot mode, all generated pivot columns)readonly string[]: Explicit list of column IDs in specified order (unmatched IDs are ignored)
// Export all columns including hidden columns
gridRef.current?.exportCsv({
columns: 'all',
});
// Export specific columns in specified order
gridRef.current?.exportCsv({
columns: ['orderNo', 'customer', 'price'],
});
4. Column Export Configuration (BGridColumn)
Configure exclusion, custom headers, and value converters on column definitions:
const columns: BGridColumn<Order>[] = [
{
id: 'orderNo',
key: 'orderNo',
label: <span>Order No</span>,
exportHeader: 'Order Number', // Custom header string instead of ReactNode
},
{
id: 'price',
key: 'price',
label: 'Price',
getExportValue: ({ value }) => Number(value), // Normalize value
},
{
id: 'actions',
key: 'orderNo',
label: 'Actions',
exportable: false, // Always excluded from export
},
];
Header Resolution Order
column.exportHeader: string or callback function receiving the column definitioncolumn.label: if string or number,String(label)- Column identifier (
columnId)
Cell Value Resolution
itemRender output is not used for export. Raw values are resolved from nested row keys (getCellValueByRowKey). If column.getExportValue is provided, its return value is used; otherwise, the raw value is exported.
5. Context Menu Integration
Use contextMenuOptions to add export actions to the right-click menu:
<BGrid<Order>
ref={gridRef}
columns={columns}
data={data}
contextMenuOptions={{
items: () => [
{
id: 'export-all-excel',
label: 'Export all loaded rows to Excel',
onSelect: () => {
gridRef.current?.exportExcel({
fileName: 'all-orders.xlsx',
rows: 'source',
columns: 'all',
});
},
},
{
id: 'export-all-csv',
label: 'Export all loaded rows to CSV',
onSelect: () => {
gridRef.current?.exportCsv({
fileName: 'all-orders.csv',
rows: 'source',
columns: 'all',
});
},
},
{ type: 'separator', id: 'export-sep' },
{
id: 'export-checked-excel',
label: 'Export checked rows to Excel',
disabled: checkedRowKeys.length === 0,
onSelect: () => {
gridRef.current?.exportExcel({
fileName: 'selected-orders.xlsx',
rows: 'checked',
columns: 'visible',
});
},
},
{
id: 'export-checked-csv',
label: 'Export checked rows to CSV',
disabled: checkedRowKeys.length === 0,
onSelect: () => {
gridRef.current?.exportCsv({
fileName: 'selected-orders.csv',
rows: 'checked',
columns: 'visible',
});
},
},
],
}}
/>
6. Excel (.xlsx) Export
You can export directly to standard OpenXML Excel (.xlsx) using gridRef.current.exportExcel without installing any heavy external dependencies:
// Export current visible data to Excel
gridRef.current?.exportExcel({
fileName: 'orders.xlsx',
sheetName: 'Orders', // Sheet tab name (default: 'Sheet1', max 31 characters)
rows: 'displayed', // 'displayed' | 'checked' | 'source' (default: 'displayed')
columns: 'visible', // 'visible' | 'all' | string[] (default: 'visible')
includeHeader: true, // Whether to include header row (default: true)
});
Numbers and boolean values are preserved as native Excel number and boolean cell types, enabling immediate formulas and calculations without manual type casting.
7. CSV Formatting and Security
exportCsv supports standard RFC 4180 escaping, UTF-8 BOM, and formula injection mitigation out of the box:
gridRef.current?.exportCsv({
fileName: 'export.csv',
delimiter: ',', // Default ',' (supports '\t', ';', etc.)
newline: '\r\n', // Default '\r\n'
bom: true, // Default true (prevents UTF-8 Korean/special char corruption in Excel)
preventFormulaInjection: true, // Default true (prefixes strings starting with '=', '+', '-', '@' with ')
});
8. Integration with SheetJS / ExcelJS
When generating binary Excel (.xlsx) files, pass getExportData() output to an external library:
SheetJS (xlsx) Example
import * as XLSX from 'xlsx';
const exportData = gridRef.current?.getExportData({
rows: 'displayed',
columns: 'visible',
});
if (exportData) {
const aoa = [
exportData.columns.map(c => c.header),
...exportData.rows.map(row => row.cells),
];
const ws = XLSX.utils.aoa_to_sheet(aoa);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
XLSX.writeFile(wb, 'orders.xlsx');
}