When using the datatable component with custom cell renderers in React, the underlying TanStack Table library expects cell content to be returned as Lit templates. This creates a problem for React users:
// ❌ This forces React users to import Lit
import { html } from 'lit'
const columns = [
{
id: 'status',
cell: (row) => html`<dap-ds-badge variant="success">${row.status}</dap-ds-badge>`
}
]
Why this is a problem:
- React developers must learn and import Lit's
htmltagged template function - Adds unnecessary Lit as a dependency in React projects
- Defeats the purpose of framework-agnostic web components
- Can't use React components in table cells without Lit
createReactCell HelperThe design system provides a createReactCell helper that allows you to render React components in datatable cells without importing Lit.
The React helper is included in the main design system package:
npm i dap-design-system
import { createReactCell } from 'dap-design-system/helpers/react'
import { DapDSDataTableReact } from 'dap-design-system/react'
import StatusBadge from './StatusBadge'
const columns = [
{
id: 'status',
header: 'Status',
cell: (info) => createReactCell({
component: StatusBadge,
props: {
status: info.row.original.status,
variant: 'success'
}
})
}
]
The createReactCell helper:
- Uses React 18's
createRoot()API to create a React root for each cell - Mounts your React component into the cell container
- Properly handles component lifecycle (creation and destruction)
- Manages cleanup when cells are removed (pagination, sorting, filtering)
- Includes an Error Boundary to gracefully handle rendering errors
interface ReactCellOptions {
component: any // React component
props?: Record<string, any>
on?: Record<string, (...args: any[]) => void>
children?: any
}
function createReactCell(
componentOrOptions: any | ReactCellOptions,
props?: Record<string, any>
): RenderCallbackContent
This example demonstrates:
- Server-side pagination
- Loading states
- React components as cell renderers
- Event handling
StatusBadge.tsx:
import { DapDSBadgeReact } from 'dap-design-system/react'
import React from 'react'
interface StatusBadgeProps {
status: string
variant: 'success' | 'warning' | 'danger'
onClick?: (status: string) => void
}
const StatusBadge: React.FC<StatusBadgeProps> = ({
status,
variant,
onClick,
}) => {
const badgeTypeMap = {
success: 'positive',
warning: 'warning',
danger: 'negative',
} as const
return (
<DapDSBadgeReact
type={badgeTypeMap[variant]}
size="sm"
onClick={() => onClick?.(status)}
style={{ cursor: onClick ? 'pointer' : 'default' }}>
{status}
</DapDSBadgeReact>
)
}
export default StatusBadge
ActionsCell.tsx:
import React from 'react'
import { DapDSButtonReact } from 'dap-design-system/react'
interface ActionsCellProps {
rowData: any
onEdit?: (rowData: any) => void
onDelete?: (rowData: any) => void
}
const ActionsCell: React.FC<ActionsCellProps> = ({
rowData,
onEdit,
onDelete,
}) => {
return (
<div style={{ display: 'flex', gap: '0.5rem' }}>
{onEdit && (
<DapDSButtonReact
variant="primary"
size="sm"
onClick={() => onEdit(rowData)}>
Edit
</DapDSButtonReact>
)}
{onDelete && (
<DapDSButtonReact
variant="primary"
size="sm"
danger={true}
onClick={() => onDelete(rowData)}>
Delete
</DapDSButtonReact>
)}
</div>
)
}
export default ActionsCell
DatatableDemo.tsx:
import { createReactCell } from 'dap-design-system/helpers/react'
import { DapDSDataTableReact } from 'dap-design-system/react'
import React, { useEffect, useState } from 'react'
import ActionsCell from './ActionsCell'
import StatusBadge from './StatusBadge'
export function DatatableDemo() {
const [users, setUsers] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 10,
})
const [rowCount, setRowCount] = useState(0)
// Fetch data with pagination
const fetchData = async () => {
setLoading(true)
await new Promise(resolve => setTimeout(resolve, 500)) // Simulate network delay
// In a real app, this would be an API call
const result = await fetch(
`/api/users?page=${pagination.pageIndex}&size=${pagination.pageSize}`
)
const data = await result.json()
setUsers(data.users)
setRowCount(data.total)
setLoading(false)
}
useEffect(() => {
fetchData()
}, [pagination])
const handleEdit = (rowData: any) => {
alert(`Editing user: ${rowData.name}`)
}
const handleDelete = (rowData: any) => {
if (confirm(`Delete user ${rowData.name}?`)) {
fetchData()
}
}
const handlePaginationChange = (event: any) => {
setPagination(event.detail.pagination)
}
// Column definitions using createReactCell
const columns = [
{ id: 'id', header: 'ID', accessorKey: 'id', size: 30 },
{ id: 'name', header: 'Name', accessorKey: 'name' },
{ id: 'email', header: 'Email', accessorKey: 'email' },
{ id: 'role', header: 'Role', accessorKey: 'role' },
{
id: 'status',
header: 'Status',
cell: (info: any) => {
const row = info.row.original
const variant =
row.status === 'active'
? 'success'
: row.status === 'pending'
? 'warning'
: 'danger'
return createReactCell({
component: StatusBadge,
props: { status: row.status, variant },
on: {
click: (status: string) =>
console.log('Status badge clicked:', status),
},
})
},
},
{
id: 'actions',
header: 'Actions',
cell: (info: any) => {
return createReactCell({
component: ActionsCell,
props: { rowData: info.row.original },
on: {
edit: (rowData: any) => handleEdit(rowData),
delete: (rowData: any) => handleDelete(rowData),
},
})
},
},
]
return (
<div style={{ padding: '2rem' }}>
<h2>React Datatable Demo with Remote Data & Pagination</h2>
<p>
This example demonstrates using React components as custom cell
renderers with server-side pagination using the{' '}
<code>createReactCell</code> helper.
</p>
{loading && users.length === 0 && (
<div style={{ padding: '2rem', textAlign: 'center', color: '#666' }}>
Loading data...
</div>
)}
<DapDSDataTableReact
data={users}
columns={columns}
rowCount={rowCount}
pager={true}
pagination={pagination}
manualPagination={true}
loading={loading}
loadingType="spinner"
onDdsPaginationChange={handlePaginationChange}
/>
</div>
)
}
createReactCell(options)Creates a React component renderer for datatable cells.
Parameters:
options.component(any, required): The React component to renderoptions.props(Object, optional): Props to pass to the componentoptions.on(Object, optional): Event handlers for component eventsoptions.children(any, optional): Child content
Shorthand syntax:
// Instead of passing options object:
createReactCell({
component: MyComponent,
props: { value: 'test' }
})
// You can use shorthand:
createReactCell(MyComponent, { value: 'test' })
Returns: A render callback that the datatable will use to mount the React component.
Events are automatically converted from lowercase to React's camelCase naming convention:
createReactCell({
component: MyComponent,
on: {
click: (data) => console.log('clicked', data), // becomes onClick
change: (data) => console.log('changed', data), // becomes onChange
customEvent: (data) => console.log('custom', data) // becomes onCustomEvent
}
})
React 18 does not handle web components properly, so you must use the React wrapper component DapDSDataTableReact instead of the native <dap-ds-datatable> web component:
// ✅ Correct - Use React wrapper
import { DapDSDataTableReact } from 'dap-design-system/react'
<DapDSDataTableReact
data={users}
columns={columns}
pager={true}
/>
// ❌ Incorrect - Don't use native web component in React
<dap-ds-datatable
data={users}
columns={columns}
pager={true}
/>
For consistency, use the React wrapper components in your cell renderers:
import { DapDSBadgeReact, DapDSButtonReact } from 'dap-design-system/react'
The createReactCell helper automatically manages component lifecycle:
- Mount: Components are created using React 18's
createRoot()when cells are rendered - Unmount: Components are properly destroyed when cells are removed (e.g., pagination, sorting)
- Cleanup: React roots are unmounted to prevent memory leaks
- Error Handling: Error Boundary catches and handles rendering errors gracefully
Each cell with a React component creates its own React root. For tables with many rows:
- Consider using virtual scrolling for large datasets
- Use
manualPaginationfor server-side pagination (recommended) - Keep cell components lightweight
The design system exports TypeScript types for the datatable:
import type {
CellContext,
ExtendedColumnDef,
RenderCallbackContent,
} from 'dap-design-system/datatable/types'
interface User {
id: number
name: string
status: string
}
const columns: ExtendedColumnDef<User>[] = [
{
id: 'status',
header: 'Status',
cell: (info: CellContext<User, unknown>) => {
const row = info.row.original
return createReactCell({
component: StatusBadge,
props: { status: row.status }
})
}
}
]
import { createReactCell } from 'dap-design-system/helpers/react'
import type { ReactCellOptions } from 'dap-design-system/helpers/react'
// Type-safe options
const options: ReactCellOptions = {
component: MyComponent,
props: {
value: 'test'
}
}
const cellRenderer = createReactCell(options)
Make sure you're using the React wrapper DapDSDataTableReact instead of the native web component.
Ensure you're using the on property in createReactCell options:
// ✅ Correct
createReactCell({
component: MyComponent,
on: {
click: handleClick
}
})
// ❌ Incorrect - this sets a prop, not an event handler
createReactCell({
component: MyComponent,
props: {
onClick: handleClick
}
})
The React helper uses flexible any types to support different React type versions across projects. This is intentional to avoid TypeScript conflicts when different projects use different versions of @types/react.
- Next.js Integration - Using native web components in Next.js
- Datatable API Reference - Full component documentation