Next.js Integration for Datatable The Advantage

Next.js (unlike React 18) can handle native web components directly, which means you can use the <dap-ds-datatable> web component without needing React wrappers. However, you'll still need the createReactCell helper to render React components inside table cells.

Setup Installation
npm i dap-design-system
Configure TypeScript for Web Components

Create or update global.d.ts in your Next.js app to add TypeScript declarations for web components:

/// <reference types="react" />

declare namespace React {
  namespace JSX {
    interface IntrinsicElements {
      'dap-ds-badge': import('dap-design-system/react-types').DapDSBadgeType
      'dap-ds-button': import('dap-design-system/react-types').DapDSButtonType
      'dap-ds-datatable': import('dap-design-system/react-types').DapDSDataTableType
      // Add other components as needed
    }
  }
}

This enables TypeScript autocomplete and type checking for web components.

Basic Usage
'use client'

import { createReactCell } from 'dap-design-system/helpers/react'
import type {
  CellContext,
  ExtendedColumnDef,
} from 'dap-design-system/datatable/types'

const columns: ExtendedColumnDef<User>[] = [
  {
    id: 'status',
    header: 'Status',
    cell: (info: CellContext<User, unknown>) => createReactCell({
      component: StatusBadge,
      props: {
        status: info.row.original.status,
        variant: 'success'
      }
    })
  }
]

export default function Page() {
  return (
    <dap-ds-datatable
      data={users}
      columns={columns}
      pager={true}
    />
  )
}
Complete Example with Remote Data

This example demonstrates:

  • Server-side pagination with Next.js API routes
  • Loading states
  • React components as cell renderers using native web components
  • Event handling
1. Create Your React Components

StatusBadge.tsx:

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 (
    <dap-ds-badge
      type={badgeTypeMap[variant]}
      size="sm"
      onClick={() => onClick?.(status)}
      style={{ cursor: onClick ? 'pointer' : 'default' }}>
      {status}
    </dap-ds-badge>
  )
}

export default StatusBadge

ActionsCell.tsx:

import React from 'react'
import type { User } from '../../../src/types/api'

interface ActionsCellProps {
  rowData: User
  onEdit?: (rowData: User) => void
  onDelete?: (rowData: User) => void
}

const ActionsCell: React.FC<ActionsCellProps> = ({
  rowData,
  onEdit,
  onDelete,
}) => {
  return (
    <div style={{ display: 'flex', gap: '0.5rem' }}>
      {onEdit && (
        <dap-ds-button
          variant="primary"
          size="sm"
          onClick={() => onEdit(rowData)}>
          Edit
        </dap-ds-button>
      )}
      {onDelete && (
        <dap-ds-button danger size="sm" onClick={() => onDelete(rowData)}>
          Delete
        </dap-ds-button>
      )}
    </div>
  )
}

export default ActionsCell
2. Create Next.js API Route

app/api/users/route.ts:

import { NextRequest, NextResponse } from 'next/server'

// Mock data generator
function generateMockUsers(page: number, pageSize: number) {
  const total = 50
  const start = (page - 1) * pageSize
  const end = Math.min(start + pageSize, total)

  const users = []
  for (let i = start; i < end; i++) {
    users.push({
      id: i + 1,
      firstName: `User${i + 1}`,
      lastName: `Last${i + 1}`,
      email: `user${i + 1}@example.com`,
      role: ['Admin', 'User', 'Editor'][i % 3],
      status: ['active', 'inactive', 'pending'][i % 3],
    })
  }

  return { users, total }
}

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const page = parseInt(searchParams.get('page') || '1')
  const pageSize = parseInt(searchParams.get('pageSize') || '10')

  // Simulate network delay
  await new Promise(resolve => setTimeout(resolve, 500))

  const result = generateMockUsers(page, pageSize)

  return NextResponse.json(result)
}
3. Set Up Your Datatable Page Component

app/datatable/page.tsx:

'use client'

import { createReactCell } from 'dap-design-system/helpers/react'
import type {
  CellContext,
  ExtendedColumnDef,
} from 'dap-design-system/datatable/types'
import { useEffect, useState } from 'react'
import ActionsCell from './components/ActionsCell'
import StatusBadge from './components/StatusBadge'

interface User {
  id: number
  firstName: string
  lastName: string
  email: string
  role: string
  status: 'active' | 'inactive' | 'pending'
}

export default function DatatablePage() {
  const [data, setData] = useState<User[]>([])
  const [loading, setLoading] = useState(true)
  const [pagination, setPagination] = useState({
    pageIndex: 0,
    pageSize: 10,
  })
  const [rowCount, setRowCount] = useState(0)

  // Fetch data from API
  const fetchData = async () => {
    setLoading(true)
    const response = await fetch(
      `/api/users?page=${pagination.pageIndex + 1}&pageSize=${pagination.pageSize}`
    )
    const result = await response.json()
    setData(result.users)
    setRowCount(result.total)
    setLoading(false)
  }

  useEffect(() => {
    fetchData()
  }, [pagination])

  const handleEdit = (user: User) => {
    alert(`Editing user: ${user.firstName} ${user.lastName}`)
  }

  const handleDelete = (user: User) => {
    if (confirm(`Delete user ${user.firstName} ${user.lastName}?`)) {
      fetchData()
    }
  }

  const handlePaginationChange = (event: any) => {
    setPagination(event.detail.pagination)
  }

  // Define columns with React components
  const columns: ExtendedColumnDef<User>[] = [
    {
      id: 'firstName',
      header: 'First Name',
      accessorKey: 'firstName',
      enableSorting: true,
    },
    {
      id: 'lastName',
      header: 'Last Name',
      accessorKey: 'lastName',
      enableSorting: true,
    },
    {
      id: 'email',
      header: 'Email',
      accessorKey: 'email',
      enableSorting: true,
    },
    {
      id: 'role',
      header: 'Role',
      accessorKey: 'role',
      enableSorting: true,
    },
    {
      id: 'status',
      header: 'Status',
      enableSorting: true,
      cell: (info: CellContext<User, unknown>) => {
        const row = info.row.original

        const variantMap: Record<string, 'success' | 'warning' | 'danger'> = {
          active: 'success',
          inactive: 'danger',
          pending: 'warning',
        }

        return createReactCell({
          component: StatusBadge,
          props: {
            status: row.status,
            variant: variantMap[row.status] || 'warning',
          },
          on: {
            click: (status: string) =>
              console.log('Status badge clicked:', status),
          },
        })
      },
    },
    {
      id: 'actions',
      header: 'Actions',
      cell: (info: CellContext<User, unknown>) => {
        const row = info.row.original

        return createReactCell({
          component: ActionsCell,
          props: { rowData: row },
          on: {
            edit: (user: User) => handleEdit(user),
            delete: (user: User) => handleDelete(user),
          },
        })
      },
    },
  ]

  return (
    <div style={{ padding: '2rem' }}>
      <h2>Next.js Datatable with Native Web Components</h2>
      <p>
        This example uses native web components directly (not React wrappers)
        with React components as custom cell renderers.
      </p>

      {loading && data.length === 0 && (
        <div style={{ padding: '2rem', textAlign: 'center', color: '#666' }}>
          Loading data...
        </div>
      )}

      <dap-ds-datatable
        data={data}
        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 Native Web Components vs React Wrappers

Next.js can use native web components directly:

// ✅ Correct for Next.js - Use native web component
<dap-ds-datatable
  data={users}
  columns={columns}
  pager={true}
/>

// ❌ Not needed - React wrapper not required in Next.js
import { DapDSDataTableReact } from 'dap-design-system/react'
<DapDSDataTableReact ... />
Use Native Web Components in Cell Renderers

For consistency with Next.js's native web component support, use native web components in your cell renderers:

// ✅ Use native web components
<dap-ds-badge type="positive">Active</dap-ds-badge>
<dap-ds-button variant="primary">Edit</dap-ds-button>

// ❌ React wrappers not needed
import { DapDSBadgeReact, DapDSButtonReact } from 'dap-design-system/react'
Client Component Required

The datatable must be used in a Client Component because it requires browser APIs and event handling:

'use client'  // Required at the top of the file

import { createReactCell } from 'dap-design-system/helpers/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
  • Cleanup: React roots are unmounted to prevent memory leaks
  • Error Handling: Error Boundary catches rendering errors gracefully
Performance Considerations

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

  • Use manualPagination for server-side pagination (recommended)
  • Consider virtual scrolling for large datasets
  • 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 }
      })
    }
  }
]
Web Component Type Declarations

Add web component types to your global.d.ts:

declare namespace React {
  namespace JSX {
    interface IntrinsicElements {
      'dap-ds-datatable': import('dap-design-system/react-types').DapDSDataTableType
      'dap-ds-badge': import('dap-design-system/react-types').DapDSBadgeType
      'dap-ds-button': import('dap-design-system/react-types').DapDSButtonType
    }
  }
}
Troubleshooting "Property does not exist on type JSX.IntrinsicElements"

Make sure you've added the web component type declarations to global.d.ts.

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
  }
})
Hydration Errors

If you encounter hydration errors, make sure:

  1. The page is marked as a Client Component with 'use client'
  2. You're not trying to use the datatable during server-side rendering
  3. Data fetching happens in useEffect, not during render
Differences from React
FeatureReact 18Next.js
Datatable ComponentDapDSDataTableReact wrapperNative <dap-ds-datatable>
Cell ComponentsReact wrappers recommendedNative web components
SetupImport React wrappersConfigure TypeScript declarations
Web Component SupportLimitedFull support
See Also