SSR & bundlers
The grid renders on the server and hydrates normally. Nothing touches window,
document or ResizeObserver outside an effect, so a page containing a grid
does not need ssr: false or a typeof window guard.
This is covered by tests that render the component to a string in a DOM-free environment — including with the toolbar, filter row, group panel, pager, summary bar and a remote data source all switched on.
What the server renders
| On the server | After hydration | |
|---|---|---|
| Root, header, ARIA roles | rendered | unchanged |
| Column captions | rendered | unchanged |
| Row window | empty | measured and filled |
| Web Worker | never constructed | constructed above the threshold |
Remote DataSource | not awaited | fetched on mount |
The row window is empty because the viewport has no size until the browser measures it. That is deliberate: guessing a row count server-side would render rows the client immediately replaces, which is worse than a single paint.
The markup still contains the grid skeleton and the column headers, so the pre-hydration page is not a blank box.
Next.js (App Router)
The grid is a client component — it holds state and subscribes to events. Mark the file that renders it:
'use client';
import { DataGrid } from '@kanunilabs/datagrid-react';
import '@kanunilabs/datagrid-react/styles.css';
export function SalesGrid({ rows }) {
return <DataGrid gridId="sales" dataSource={rows} rowKey="id" columns={columns} />;
}
A server component can fetch the rows and pass them down as a prop; only the grid itself needs the client boundary.
Import the stylesheet once — in the root layout or in the client component. Next deduplicates it either way.
The worker in Next.js
Nothing to do. The worker is compiled into the package and starts from a Blob URL, so Next never has to resolve a worker file out of a dependency — which it does not do.
Two cases still need the file, and it ships in the package for them:
- a Content-Security-Policy without
worker-src … blob:— the browser refuses the Blob, and only a real URL will start the worker; - serving the worker from a CDN, so it caches separately from your bundle.
cp node_modules/@kanunilabs/datagrid-core/dist/datagrid.worker.js public/
<DataGrid worker={{ url: '/datagrid.worker.js' }} /* ... */ />
If the worker cannot start for any reason the grid still works: it runs the same pipeline on the main thread and warns once in development. The only difference is that a large sort blocks the UI instead of running off-thread.
Vite
Also nothing to do. If you need the file route (CSP or CDN, as above):
import workerUrl from '@kanunilabs/datagrid-core/dist/datagrid.worker.js?url';
<DataGrid worker={{ url: workerUrl }} /* ... */ />
Other bundlers
Any setup that can serve a static .js file works: copy
node_modules/@kanunilabs/datagrid-core/dist/datagrid.worker.js somewhere
public and pass its URL. The file is self-contained — it has no imports of its
own, so no additional chunk resolution is involved.
Turning the worker off
<DataGrid worker={{ enabled: false }} /> {/* always main thread */}
<DataGrid worker={{ threshold: 50_000 }} /> {/* offload later than the default 20.000 */}
Below the threshold the main thread is genuinely faster — the round trip costs more than the sort saves — so small grids never use the worker regardless.
Content Security Policy
The worker is loaded from your own origin (you copied it there), so a
worker-src 'self' policy is enough. The grid does not use eval, inline
scripts or remote resources.
Next: performance.