React Integration for Datatable The Problem

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 html tagged 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
The Solution: createReactCell Helper

The design system provides a createReactCell helper that allows you to render React components in datatable cells without importing Lit.

Installation

The React helper is included in the main design system package:

npm i dap-design-system
Basic Usage
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'
      }
    })
  }
]
How It Works

The createReactCell helper:

  1. Uses React 18's createRoot() API to create a React root for each cell
  2. Mounts your React component into the cell container
  3. Properly handles component lifecycle (creation and destruction)
  4. Manages cleanup when cells are removed (pagination, sorting, filtering)
  5. Includes an Error Boundary to gracefully handle rendering errors
Type Signature
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
Complete Example with Remote Data

This example demonstrates:

  • Server-side pagination
  • Loading states
  • React components as cell renderers
  • Event handling
1. Create Your React Components

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
2. Set Up Your Datatable Component

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>
  )
}
API Reference createReactCell(options)

Creates a React component renderer for datatable cells.

Parameters:

  • options.component (any, required): The React component to render
  • options.props (Object, optional): Props to pass to the component
  • options.on (Object, optional): Event handlers for component events
  • options.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.

Event Handling

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
  }
})
Important Notes React 18 Compatibility

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}
/>
Using React Wrapper Components

For consistency, use the React wrapper components in your cell renderers:

import { DapDSBadgeReact, DapDSButtonReact } from 'dap-design-system/react'
Component Lifecycle

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
Performance Considerations

Each cell with a React component creates its own React root. For tables with many rows:

  • Consider using virtual scrolling for large datasets
  • Use manualPagination for server-side pagination (recommended)
  • Keep cell components lightweight
TypeScript Support Importing Types

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 }
      })
    }
  }
]
Type-Safe Cell Renderers
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)
Troubleshooting Components Not Rendering

Make sure you're using the React wrapper DapDSDataTableReact instead of the native web component.

Events Not Firing

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
  }
})
Type Errors Across React Versions

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.

See Also