Lattice Grid Buy a licence

react data grid · excel export

React Grid: Excel Export

grid.export.excel() writes a real, styled workbook in the browser: frozen panes, a named sheet, banded row fills. Wire it to the built-in toolbar action, your own button, or both, reached through the same ref every imperative call in React goes through.

Also in: Angular

Install and import

npm install @toclocoinc/lattice-grid
# free on localhost; a production domain needs a licence key (see /pricing/)

The files

lattice.ts

// lattice.ts
import React from 'react';
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createLatticeGrid } from '@toclocoinc/lattice-grid/modules/react';

export const LatticeGrid = createLatticeGrid({ React, createGrid });

SupplierCharges.tsx

// SupplierCharges.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { LatticeGrid } from './lattice';

const columns = [
  { field: 'ref', title: 'Invoice', layout: { pin: 'start', width: 140 } },
  { field: 'supplier', title: 'Supplier', layout: { flex: 1, min: 200 }, filter: { type: 'set' } },
  { field: 'region', title: 'Region', filter: { type: 'set' } },
  { field: 'category', title: 'Category', filter: { type: 'set' } },
  { field: 'quantity', title: 'Qty', type: 'number', layout: { width: 110 } },
  { field: 'amount', title: 'Amount', type: 'number', layout: { width: 150 },
    format: { style: 'currency', currency: 'GBP', decimals: 2 }, total: 'sum', filter: { type: 'number' } },
  { field: 'note', title: 'Note', layout: { width: 280 } },
];

// The workbook is written in the browser with no dependency: a zip
// container, the sheet, the shared strings and the styles, all assembled by
// the grid. Nothing is uploaded and nothing is fetched.
const toolPanel = {
  side: 'left',
  panels: ['columns', 'filters', 'quick'],
  exportName: 'supplier-charges',
  actions: [
    'undo', 'redo', 'restore', 'maximise', '-',
    'excel',
    {
      name: 'excel-named',
      title: 'Download .xlsx, named sheet with a frozen header',
      icon: 'spreadsheet',
      run: ({ grid }: any) => grid.export.excel({
        download: true,
        fileName: 'supplier-charges-q3',
        sheetName: 'Charges',
        freezePanes: true,
        variantFills: true,
      }),
    },
    'clipboard',
  ],
};

const rows = [/* supplier charge records, one per invoice line */];

function SupplierCharges() {
  const gridRef = React.useRef<{ grid: any } | null>(null);

  // The same writer, reached imperatively through the ref: a named sheet
  // with a frozen header, triggered from your own button.
  const download = () => gridRef.current?.grid.export.excel({
    download: true,
    fileName: 'supplier-charges-q3',
    sheetName: 'Charges',
    freezePanes: true,
    variantFills: true,
  });

  return (
    <>
      <button onClick={download}>Download .xlsx</button>
      <LatticeGrid
        ref={gridRef}
        rowKey="id"
        selection="multiple"
        toolPanel={toolPanel}
        columns={columns}
        rows={rows}
        style={{ height: '460px', marginTop: '8px' }}
      />
    </>
  );
}

createRoot(document.getElementById('supplier-charges')!).render(<SupplierCharges />);

See it running

Building…
Loading a live grid…

Open this demo's React tab →

Working in React

Two routes to the same writer. The toolbar's built-in 'excel' action downloads with the grid's defaults; a named custom action or your own button both call grid.export.excel(options) directly, reached through ref.current.grid after mount. Neither route needs a server: the workbook is assembled entirely in the tab.

Only what the reader can see. Export follows the current filter and sort by default, so a filtered view exports the filtered rows, not the full dataset behind it.

StrictMode is handled the same way editing is: the development double-mount destroys the first grid instance in its effect cleanup, so a second click on Download always reaches the grid actually on screen.

Related