Documentation

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 serverAfter hydration
Root, header, ARIA rolesrenderedunchanged
Column captionsrenderedunchanged
Row windowemptymeasured and filled
Web Workernever constructedconstructed above the threshold
Remote DataSourcenot awaitedfetched 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.