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.
npm i dap-design-system
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.
'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}
/>
)
}
This example demonstrates:
- Server-side pagination with Next.js API routes
- Loading states
- React components as cell renderers using native web components
- Event handling
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
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)
}
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>
)
}
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
}
})
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 ... />
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'
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'
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
Each cell with a React component creates its own React root. For tables with many rows:
- Use
manualPaginationfor server-side pagination (recommended) - Consider virtual scrolling for large datasets
- 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 }
})
}
}
]
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
}
}
}
Make sure you've added the web component type declarations to global.d.ts.
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
}
})
If you encounter hydration errors, make sure:
- The page is marked as a Client Component with
'use client' - You're not trying to use the datatable during server-side rendering
- Data fetching happens in
useEffect, not during render
| Feature | React 18 | Next.js |
|---|---|---|
| Datatable Component | DapDSDataTableReact wrapper | Native <dap-ds-datatable> |
| Cell Components | React wrappers recommended | Native web components |
| Setup | Import React wrappers | Configure TypeScript declarations |
| Web Component Support | Limited | Full support |
- React Integration - Using React wrappers
- Datatable API Reference - Full component documentation