@ascentsparksoftware/react-datatables-net
v1.1.0
Published
Modern React wrapper for DataTables (datatables.net) using its non-jQuery API. Typed cells, free edit-in-place, styling adapters. Next.js and React Compiler friendly.
Maintainers
Readme
@ascentsparksoftware/react-datatables-net
A React wrapper for DataTables.net, by Ascentspark
Typed React components for DataTables.net — no jQuery in your code
🚀 Live demo · ✏️ Edit in place · 📦 npm · 📝 Changelog
DataTables turns a plain HTML table into an interactive one: sorting,
search, pagination, and a large set of extensions. @ascentsparksoftware/react-datatables-net lets you drive it from
React without writing any jQuery. Render a <DataTable> component, pass your options and data as
props, and read events back as typed callbacks. Client-only by design, StrictMode-safe, React
Compiler-safe, Next.js friendly, and escaped by default. It is the React sibling of our Angular
wrapper, ngx-datatables-net, and shares its
design contracts.
⚛️ Built the modern React way. Function components, portals for live React cell content, and DataTables' non-jQuery constructor API. Your only peer dependencies are React and DataTables: no
jqueryrequired of your app. The engine is loaded lazily inside an effect, so jQuery and DataTables stay out of your initial bundle and out of the Next.js server graph. (DataTables uses jQuery internally, so it still ships as a transitive dependency, but you never write or configure it.)
🆓 Built-in inline editing, an alternative to the paid version
DataTables' own inline editing ships only in Editor, a commercial paid extension. This package gives you double-click edit-in-place for free (MIT): text, textarea, number, date, checkbox, select, multiselect and custom React component editors, with validation, keyboard navigation and optional pessimistic async saving, all through the non-jQuery API and written the React way.
✨ Why @ascentsparksoftware/react-datatables-net
- No jQuery in your code. Built on the DataTables non-jQuery API; jQuery stays a transitive dependency you never touch.
- Client-only with lazy engine loading. The DataTables constructor is dynamically imported inside an effect, so importing the module on the server (Next.js does, even for never-rendered components) touches no DOM, and jQuery + the engine stay out of the initial bundle and the server graph.
- Props in, callbacks out. Pass a new
dataarray and the table reconciles itself via the cheapclear → rows.add → drawpath — no manual redraw trigger to remember. Events come back as typed callback props; the underlyingApiis onerefaway. - Live React cell content. A column's
cellrender prop renders real React — hooks, context and event handlers all work — while sorting and search keep operating on the raw data. - Free edit-in-place. Inline editing without the commercial Editor licence (see the callout above).
- StrictMode-safe and React Compiler-safe. Init is idempotent, cleanup is complete, and nothing mutated is read during render — the dev double-mount never leaks an instance or a listener.
- Safe by default.
escapeByDefaultescapes every column without an explicit renderer;unsafeHtmlRenderer()is the explicit, named escape hatch for trusted HTML. - Every extension works. Buttons, Select, Responsive, FixedHeader/Columns, Scroller, RowGroup, SearchPanes, ColumnControl and more: pass the config straight through.
- Four styling adapters. DataTables default, Bootstrap 5, plus Tailwind and Material themes authored by us (DataTables ships neither).
📦 Install
npm install @ascentsparksoftware/react-datatables-net datatables.net datatables.net-dtAdd the stylesheet for your chosen theme once, e.g. in app/layout.tsx (Next.js) or main.tsx
(Vite):
import 'datatables.net-dt/css/dataTables.dataTables.css';🚀 Quick start
Configure the provider once, near the root of your app:
// providers.tsx
import { DataTablesProvider } from '@ascentsparksoftware/react-datatables-net';
import { defaultStyling } from '@ascentsparksoftware/react-datatables-net/dt';
<DataTablesProvider styling={defaultStyling} escapeByDefault options={{ pageLength: 10 }}>
<App />
</DataTablesProvider>Then render a table:
'use client';
import { useState } from 'react';
import { DataTable, type Api, type ConfigColumns } from '@ascentsparksoftware/react-datatables-net';
interface Employee {
id: number;
name: string;
position: string;
office: string;
}
const columns: ConfigColumns[] = [
{ data: 'id', title: 'ID' },
{ data: 'name', title: 'Name' },
{ data: 'position', title: 'Position' },
{ data: 'office', title: 'Office' },
];
export default function EmployeesPage() {
const [data] = useState<Employee[]>([
{ id: 1, name: 'Ada Lovelace', position: 'Engineer', office: 'London' },
{ id: 2, name: 'Linus Torvalds', position: 'Maintainer', office: 'Portland' },
]);
return (
<DataTable<Employee>
className="display"
style={{ width: '100%' }}
data={data}
columns={columns}
onInit={(api: Api<Employee>) => {
// escape hatch: the full DataTables Api for imperative calls
}}
onRowClick={({ row }) => {
// row is the typed Employee that was clicked
}}
>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Position</th>
<th>Office</th>
</tr>
</thead>
</DataTable>
);
}To reload the table later, set a new array into state and pass it as data. That is the whole loop.
✏️ Edit in place (free, no Editor licence)
Give a column an editor. Double-click to edit; Enter commits, Escape cancels, Tab moves to the
next editable cell.
import { DataTable, type DtColumn } from '@ascentsparksoftware/react-datatables-net';
const columns: DtColumn<User>[] = [
{ data: 'name', title: 'Name', editor: { type: 'text', validate: v => String(v).trim() ? null : 'Required' } },
{ data: 'role', title: 'Role', editor: { type: 'select', options: ROLES } },
{ data: 'skills', title: 'Skills', editor: { type: 'multiselect', options: SKILLS } },
];
<DataTable<User> data={users} columns={columns} save={saveHandler} />Editors: text, textarea, number, date, checkbox, select, multiselect, custom
(your own React component). Pass a save handler to persist before the cell is written (sync or
Promise). The cell shows a busy state and, on failure, stays open with an inline error for retry.
Full details in the Edit in place guide.
🧩 <DataTable> props
All props are optional. T is the row type (<DataTable<Employee> …>).
| Prop | Type | Description |
| --- | --- | --- |
| options | Config | DataTables options object. A structural change (new keys/values, not a new inline literal that serializes identically) recreates the table. Merged over the provider's default options. |
| data | readonly T[] | Row data. A new array reference replaces the rows without re-initializing (clear → rows.add → draw(false), keeping the paging position). |
| columns | DtColumn<T>[] | Column definitions (convenience for options.columns). Columns may carry a cell render prop and/or an editor (see the table below). Structural change recreates the table. |
| escapeByDefault | boolean | Override the provider's escapeByDefault for this table (see Security). |
| styling | DtStyling | Override the provider's styling adapter for this table. |
| className | string | Class applied to the <table> element (e.g. display for the default theme). |
| style | CSSProperties | Inline style for the <table> element. |
| id | string | id for the <table> element. |
| children | ReactNode | Table markup: <thead> (and optionally <tfoot> / a DOM-sourced <tbody>). |
| ref | Ref<DataTableRef<T>> | Imperative access to the DataTables Api (ref.current.api, null until initialized). |
| onInit | (api: Api<T>) => void | Called once with the Api immediately after the table is created. |
| onDraw | (event: DtEvent<T>) => void | DataTables draw event. event.page carries an api.page.info() snapshot. |
| onPage | (event: DtEvent<T>) => void | DataTables page event. event.page carries an api.page.info() snapshot. |
| onXhr | (event: DtEvent<T>) => void | DataTables xhr event (Ajax/server-side data load). |
| onSelect | (event: DtSelectEvent<T>) => void | Select extension select event. |
| onDeselect | (event: DtSelectEvent<T>) => void | Select extension deselect event. |
| onSelectionChange | (selected: readonly T[]) => void | Current selection (row data) after every select/deselect. Requires the Select extension. |
| onRowClick | (event: DtRowClickEvent<T>) => void | Row click (delegated listener on <tbody>), with resolved row data, index, <tr> element and the mouse event. |
| save | DtCellSaveHandler<T> | Optional pessimistic save handler for edit-in-place. Runs on commit before the cell is written; return a Promise to defer the write, reject/throw to keep the cell unchanged and leave the editor open for retry. |
| onCellEditStart | (ctx: DtEditContext<T>) => void | Emitted when an edit begins (editor opened). |
| onCellEdit | (commit: DtCellEditCommit<T>) => void | Emitted after a changed value is validated, saved and written to the cell. |
| onCellEditCancel | (evt: DtCellEditCancel<T>) => void | Emitted when an edit is abandoned (Escape, blur with no change, programmatic close, …). |
| onCellEditError | (evt: DtCellEditError<T>) => void | Emitted when a save handler fails; the cell is left unchanged. |
Inline callback props are safe: the component reads the latest callbacks from a ref, so a new inline
function never forces a re-initialization. The same applies to a column's cell render prop and
editor callbacks — swapping the function (same column structure) takes effect on the next draw
without recreating the table. The styling prop is compared by a structural signature too, so an
inline withTailwind({...}) per render does not recreate the table.
Server-side tip: a
data: nullaction column should either carry acellrender prop (the component's display shim supplies the cell content) or set DataTables'defaultContent— otherwise DataTables warns about the missing data source inserverSidemode.
DtColumn<T> extras
DtColumn<T> is DataTables' own ConfigColumns plus two fields:
| Field | Type | Description |
| --- | --- | --- |
| cell | (ctx: DtCellContext<T>) => ReactNode | React render prop for the cell's display content. Renders as live React (portals from the component's own tree, so hooks, context and event handlers work), re-rendered across draws. Sorting/filtering still use the raw data. Context: { cellData, row, rowIndex, colIndex }. |
| editor | DtEditorConfig<T> | Edit-in-place configuration; omit for a read-only column. |
Typed Api helpers
| Helper | Signature | Description |
| --- | --- | --- |
| ajaxParams | (api: Api<T>) => AjaxData \| undefined | Typed accessor for api.ajax.params() (upstream types it as a bare object): the last Ajax/server-side request parameters, or undefined when the table has no Ajax source. |
| pageInfo | (api: Api<T>) => ApiPageInfo | Typed convenience for api.page.info(). |
Editor kinds
| editor.type | Control | Committed value |
| --- | --- | --- |
| text | <input type="text"> | string |
| textarea | <textarea> | string (commit with Ctrl/⌘+Enter, or blur) |
| number | <input type="number"> | number, or null when blank |
| date | <input type="date"> | ISO yyyy-mm-dd string, or null when blank |
| checkbox | checkbox | boolean |
| select | <select> | the chosen option's value |
| multiselect | <select multiple> | an array of the chosen option values |
| custom | your React component | whatever your component passes to commit() |
All editors share the optional ariaLabel, disabled: (ctx) => boolean and
validate: (value, row) => string \| null \| undefined fields. See the
Edit in place guide for everything.
🎨 Styling adapters
Pick one adapter and pass it to DataTablesProvider (or per table via the styling prop). Each
adapter lazily imports its DataTables constructor, so nothing touches the DOM at module import time.
DataTables default theme — @ascentsparksoftware/react-datatables-net/dt
import { defaultStyling } from '@ascentsparksoftware/react-datatables-net/dt';
// stylesheet (once, in your root layout / entry):
import 'datatables.net-dt/css/dataTables.dataTables.css';
<DataTablesProvider styling={defaultStyling}>…</DataTablesProvider>Bootstrap 5 — @ascentsparksoftware/react-datatables-net/bs5 (needs the datatables.net-bs5 peer)
import { bootstrap5Styling } from '@ascentsparksoftware/react-datatables-net/bs5';
// stylesheets:
import 'bootstrap/dist/css/bootstrap.css';
import 'datatables.net-bs5/css/dataTables.bootstrap5.css';
// tables typically use className="table table-striped" instead of "display"
<DataTablesProvider styling={bootstrap5Styling}>…</DataTablesProvider>Tailwind — @ascentsparksoftware/react-datatables-net/tailwind, authored by us (DataTables ships no official
Tailwind package). Self-contained, scoped stylesheet — Tailwind v4-compatible plain CSS, no
Tailwind build step required, themeable via CSS custom properties:
import { tailwindStyling } from '@ascentsparksoftware/react-datatables-net/tailwind';
// stylesheet:
import '@ascentsparksoftware/react-datatables-net/tailwind/styles/react-datatables-net.tailwind.css';
<DataTablesProvider styling={tailwindStyling}>…</DataTablesProvider>Material — @ascentsparksoftware/react-datatables-net/material, authored by us (DataTables ships no official
Material package). Follows Material Design conventions, themeable via CSS custom properties:
import { materialStyling } from '@ascentsparksoftware/react-datatables-net/material';
// stylesheet:
import '@ascentsparksoftware/react-datatables-net/material/styles/react-datatables-net.material.css';
<DataTablesProvider styling={materialStyling}>…</DataTablesProvider>The Tailwind and Material adapters scope their stylesheets under a class (rdt-tailwind /
rdt-material) that the component adds to the DataTables container automatically.
Overriding the scope class detaches the shipped stylesheet. The CSS is hard-scoped to the default class, so
withTailwind({ scopeClass: 'my-scope' })renders the table unstyled unless you also ship your own copy of the stylesheet rescoped under.my-scope. To theme the table, keep the default scope and override the CSS custom properties (--rdt-tw-*for Tailwind) instead.
📚 Documentation
Live demo and full docs for every feature, styling adapter and extension: react-datatables-net.ascentspark.com
In this repo:
| Doc | What's inside | | --- | --- | | Edit in place | the editing engine in full | | Security | cell rendering and the XSS boundary | | Changelog | dated release notes |
🔢 Versions
@ascentsparksoftware/react-datatables-net uses independent semantic versioning (unlike its Angular sibling, it does not
need one release line per framework major). Peer dependencies: react / react-dom ^19 and
datatables.net ^2.1; the datatables.net-dt and datatables.net-bs5 peers are optional — install
the one your chosen styling adapter needs.
🛠️ Working on this repo
This is an npm workspace: package/ is the publishable library, demo/ is the Next.js demo app.
npm install
npm run build -w @ascentsparksoftware/react-datatables-net # build the library
npm run dev -w demo # run the demo (http://localhost:4288)
npm test -w @ascentsparksoftware/react-datatables-net # unit tests (Vitest)
npm run build:demo && npx playwright test # end-to-end tests🤝 Help keep it healthy
We genuinely try to keep this library current, bug-free and secure, and honestly the best way to get there is together. If something breaks, please open an issue so we can look into it. If you can fix a bug or add something useful, pull requests are very welcome, big or small. An open-source library stays dependable only when people use it, tell us what's broken, and pitch in now and then, so thank you in advance for anything you send our way. 💛
👋 About
We are Ascentspark. We have used datatables.net across our projects and kept writing the same wrapper code to make it feel at home in modern React. This is that code, tidied up and shared, in case you are looking for a React wrapper too.
License
MIT
