// src/components/DataTable/types.ts export interface ColumnDef<T> { key: keyof T; header: string; render?: (item: T) => React.ReactNode; }
export interface DataTableProps<T> { data: T[]; columns: ColumnDef<T>[]; children: React.ReactNode; // For compound component structure }
export interface DataTableRowProps<T> { item: T; columns: ColumnDef<T>[]; children?: React.ReactNode; // Allows custom row content, or defaults to cells }
export interface DataTableColumnHeaderProps<T> { column: ColumnDef<T>; }
export interface DataTableColumnCellProps<T> { item: T; column: ColumnDef<T>; }
// src/components/DataTable/index.tsx import React, { createContext, useContext } from 'react'; import { ColumnDef, DataTableProps, DataTableRowProps, DataTableColumnHeaderProps, DataTableColumnCellProps, } from './types';
interface DataTableContextType<T> { data: T[]; columns: ColumnDef<T>[]; }
const DataTableContext = createContext<DataTableContextType<any> | undefined>(undefined);
function useDataTableContext<T>() { const context = useContext(DataTableContext); if (!context) { throw new Error('DataTable compound components must be rendered within a <DataTable> component'); } return context as DataTableContextType<T>; }
const DataTable = <T,>({ data, columns, children }: DataTableProps<T>) => { return ( <DataTableContext.Provider value={{ data, columns }}> <table> {children} </table> </DataTableContext.Provider> ); };
const DataTableHeader = <T,>() => { const { columns } = useDataTableContext<T>(); return ( <thead> <tr> {columns.map((column) => ( <DataTable.HeaderCell key={String(column.key)} column={column} /> ))} </tr> </thead> ); };
const DataTableHeaderCell = <T,>({ column }: DataTableColumnHeaderProps<T>) => { return <th>{column.header}</th>; };
const DataTableBody = <T,>() => { const { data, columns } = useDataTableContext<T>(); return ( <tbody> {data.length === 0 ? ( <tr> <td colSpan={columns.length}>No data available.</td> </tr> ) : ( data.map((item, index) => ( <DataTable.Row key={index} item={item} columns={columns} /> )) )} </tbody> ); };
const DataTableRow = <T,>({ item, columns }: DataTableRowProps<T>) => { return ( <tr> {columns.map((column) => ( <DataTable.Cell key={String(column.key)} item={item} column={column} /> ))} </tr> ); };
const DataTableColumnCell = <T,>({ item, column }: DataTableColumnCellProps<T>) => { const cellContent = column.render ? column.render(item) : (item[column.key] as React.ReactNode); return <td>{cellContent ?? '-'}</td>; // Handle null/undefined gracefully };
DataTable.Header = DataTableHeader; DataTable.HeaderCell = DataTableHeaderCell; DataTable.Body = DataTableBody; DataTable.Row = DataTableRow; DataTable.Cell = DataTableColumnCell;
export { DataTable };
// src/App.tsx (Example Usage) import React from 'react'; import { DataTable } from './components/DataTable';
interface Product { id: string; name: string; category: string; price: number; stock: number | null; }
const products: Product[] = [ { id: 'p1', name: 'Laptop Pro', category: 'Electronics', price: 1200, stock: 15 }, { id: 'p2', name: 'Keyboard Mech', category: 'Electronics', price: 150, stock: 0 }, { id: 'p3', name: 'Monitor Ultra', category: 'Peripherals', price: 400, stock: 5 }, { id: 'p4', name: 'Mouse Ergo', category: 'Peripherals', price: 75, stock: null }, ];
const productColumns = [ { key: 'id', header: 'Product ID' }, { key: 'name', header: 'Product Name' }, { key: 'category', header: 'Category' }, { key: 'price', header: 'Price', render: (product: Product) => $${product.price.toFixed(2)} }, { key: 'stock', header: 'Stock', render: (product: Product) => ( <span style={{ color: product.stock === 0 ? 'red' : 'green' }}> {product.stock === null ? 'N/A' : product.stock} </span> ), }, ];
function App() { return ( <div style={{ padding: '20px' }}> <h1>Product List</h1> <DataTable<Product> data={products} columns={productColumns}> <DataTable.Header /> <DataTable.Body /> </DataTable>
<h2>Empty Data Example</h2> <DataTable<Product> data={[]} columns={productColumns}> <DataTable.Header /> <DataTable.Body /> </DataTable> </div> ); }
export default App;
Short Explanation: The core of this design relies on TypeScript generics, specifically DataTable<T>. The generic type T is passed down through the DataTableContext to all sub-components. This allows ColumnDef<T> to correctly type keyof T, ensuring that only valid keys for the data object T can be specified. Custom render functions in ColumnDef and DataTable.Cell also receive T, providing full type inference for the data item being rendered. This cascade of generics ensures that type safety is maintained from the top-level data prop down to the individual cell rendering, preventing common type mismatches.
Edge Cases:
- Empty Data Sets: The
DataTable.Body component explicitly checks data.length === 0 and renders a "No data available" message spanning all columns, improving user experience. - `undefined`/`null` Values:
DataTable.Cell uses the nullish coalescing operator (?? '-') to display a hyphen for null or undefined cell values when no custom renderer is provided. Custom renderers offer full control for specific handling (e.g., N/A for null stock). - Custom Renderers: Both
ColumnDef and DataTable.Cell support a render prop. This allows consumers to define how specific column data should be displayed, enabling rich UI elements or formatted values (e.g., currency, status indicators) while preserving the underlying data type.
Tests Outline:
* Verify that DataTable correctly infers T from the data prop. * Ensure ColumnDef.key only accepts valid keys of T. * Test that DataTable.Cell's item prop is correctly typed as T. * Confirm custom render functions receive the correct T type for their item argument. * Assert that providing an invalid key to ColumnDef results in a TypeScript error.
* Render DataTable with sample data and verify all headers and data cells are present. * Test custom render functions are correctly applied to format cell content. * Verify the "No data available" message appears when data is an empty array. * Check how null or undefined values are rendered by default and with custom renderers. * Ensure DataTable.Row and DataTable.Cell correctly display data for each item and column.
- Compound Component Structure Tests:
* Verify that DataTable.Header, DataTable.Body, DataTable.Row, DataTable.Cell throw an error if used outside of DataTable. * Ensure context propagation works as expected.