react data grid · server-side with duckdb
React Grid: Server-Side DuckDB
createPushdownSource plus duckdbAdapter push the grid's filter, group and total work down into a real analytical engine instead of computing over rows already sitting on screen, and fullDataset says the subtotal is over the whole matching set, not one page of it.
Also in: Angular
Install and import
npm install @toclocoinc/lattice-grid
# DuckDB itself loads from a CDN at runtime, not from this package
# free on localhost; a production domain needs a licence key (see /pricing/)
The files
lattice.ts
// lattice.ts
import React from 'react';
import * as Lattice from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createLatticeGrid } from '@toclocoinc/lattice-grid/modules/react';
export const { createGrid, createPushdownSource, duckdbAdapter } = Lattice;
export const LatticeGrid = createLatticeGrid({ React, createGrid });
Readings.tsx
// Readings.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import * as Lattice from '@toclocoinc/lattice-grid';
import { LatticeGrid, createPushdownSource, duckdbAdapter } from './lattice';
// The share of a group's rows inside tolerance. Under fullDataset the scope
// is the whole group, so a subtotal is a whole-group rate, not a page's.
const inSpecRate = (values: unknown[]) => {
let seen = 0, ok = 0;
for (const v of values) { if (typeof v === 'boolean') { seen++; if (v) ok++; } }
return seen ? ok / seen : null;
};
const columns = [
{ field: 'plant', title: 'Plant', filter: { type: 'set' }, group: { enabled: true, index: 0 }, total: 'count' },
{ field: 'line', title: 'Line', filter: { type: 'set' } },
{ field: 'product', title: 'Product' },
{ field: 'bore_mm', title: 'Bore', type: 'millimetres', spec: { lower: 9.95, upper: 10.05, target: 10 }, total: 'avg' },
{ field: 'cycle_seconds', title: 'Cycle', type: 'seconds', total: 'avg' },
{ id: 'in_spec_rate', field: 'in_spec', title: 'In-spec rate', type: 'number', format: { style: 'percent', decimals: 2 }, total: inSpecRate },
{ field: 'taken_at', title: 'Taken', type: 'dateString' },
];
function Readings() {
const [source, setSource] = React.useState<any>(null);
const [status, setStatus] = React.useState('Starting DuckDB in the browser...');
// DuckDB is your own engine, loaded from a CDN; the grid imports none of
// it. duckdbAdapter takes the connection you made, so the source drives a
// full analytical engine while the grid package stays dependency-free.
React.useEffect(() => {
let disposed = false;
let connection: any, worker: any;
(async () => {
try {
setStatus('Loading DuckDB from a CDN...');
const duckdb = await import('https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@1.29.0/+esm');
const bundle = await duckdb.selectBundle(duckdb.getJsDelivrBundles());
worker = await duckdb.createWorker(bundle.mainWorker);
const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(duckdb.LogLevel.ERROR), worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
if (disposed) return;
connection = await db.connect();
// The engine resolves the URL, so it must be absolute.
const file = new URL('/demo-data/readings.parquet', location.origin).href;
const adapter = duckdbAdapter({ connection, from: `read_parquet('${file}')` });
// fullDataset pulls the whole matching set in once and holds it, so
// the subtotals and the grand total are over every matching row, not
// one page of them.
setSource(createPushdownSource({
adapter, compute: Lattice, pageSize: 200,
fullDataset: { enabled: true, maxRows: 250000 },
}));
setStatus('Rows pulled into the tab and grouped by plant.');
} catch {
setStatus('DuckDB could not start here: it needs WebAssembly and a CDN connection.');
}
})();
return () => {
disposed = true;
try { connection?.close?.(); } catch {}
try { worker?.terminate?.(); } catch {}
};
}, []);
return (
<>
<div style={{ fontSize: 13, color: 'var(--ink-2)' }}>{status}</div>
{source && (
<LatticeGrid
rowKey="reading_id"
toolPanel={{ side: 'left', panels: ['filters', 'columns'] }}
grandTotalRow="bottom"
groupFooter
showTotalInHeader
columns={columns}
source={source}
style={{ height: '520px', marginTop: '12px' }}
/>
)}
</>
);
}
createRoot(document.getElementById('readings')!).render(<Readings />);
See it running
Working in React
The source is state, not a ref. Building the DuckDB connection is asynchronous,
so the grid is not rendered at all until source is set; there is no grid instance to
reach before that, by ref or otherwise.
Clean up the connection. The effect's cleanup closes the DuckDB connection and terminates its worker, so navigating away mid-query does not leave a worker running in a tab nobody is looking at.
compute: Lattice. The pushdown source still needs the grid's own compute helpers for anything DuckDB itself is not asked to do, which is why the whole package namespace is passed in alongside the adapter.
Related
Read the docs React data grid overview Free on localhost. See pricing.