qweezz
qweezz

Reputation: 804

How to make a reusable table component which can handle different data structures and layouts?

I'm building an app with many tables. Generally they look the same, but some has different columns set or cell content. I'd like to make a reusable table component which can be used through out the entire app.

At the moment I made a table wrapper component which accepts children and table type. Columns are rendered depending on table type. Then I have several components each for different table because data structure may differ and layout might be slightly different. The question is how to make a single table component which can handle different data structures and different layouts?

I think it can be done with conditional rendering (but I do not want excessively use this because it will be hard to maintain and difficult to read):

  {tableType === "first" ? <TableCell>{item.name}</TableCell> : null}
  {tableType === "second" ? <TableCell>{item.status}</TableCell> : null}

I was told that it can be done in somekind of this way:

<TableCell>{getCellComponent(tableType, value)}</TableCell>

Unfortunatelly I'm not that smart enough to make it myself. I understand the general idea for this approach but don't understand how to make it. Can someone help me with that please?

I made a Codesandbox with two simplified tables: https://codesandbox.io/s/complex-table-yrrv6c?file=/src/App.tsx

UPD

I went with @Dilshan solution. It works great. Meanwhile there're couple TS errors which I don't know how to fix.

I want to store columns props in a variable. Like this:

const columnObj = {
  firstName: {
    name: "First Name",
    width: "25%",
    accessor: (payload: any) => (
      <>
        <Avatar />
        {payload.who.nickname}
      </>
    )
  },
  // ...
};

But then what type should I specify for payload?

I'd like to pass onClick handler to the table. Basically I want whole row to be clickable:

  <MyTable<PayloadTyep2>
    columns={columnObj}
    payload={secondTableData}
    onClick={(id) => console.log("Row clicked:", id)}
  />

Then in assign it in Table component:

  <TableBody>
    {payload.map((rowData, index) => {
      return (
        <TableRow key={index} 
         onClick={(id) => props.onClick(rowData.id)}> // here get TS error
          // ...
        </TableRow>
      );
    })}
  </TableBody>

How to fix that?

Here's forked Codesanbox: https://codesandbox.io/s/react-mui-reusable-table-forked-vk7h6o?file=/src/App.tsx

Upvotes: 1

Views: 5236

Answers (4)

The KNVB
The KNVB

Reputation: 3844

I give you an example for your reference:

First you may define columnList object:

 let columnList = [
        { label: "Post", accessor: "post" },
        { label: "Name", accessor: "name" },
        { label: "Email", accessor: "email" },
        { label: "Primary Phone No", accessor: "primaryPhoneNo" },
        { label: "Secondary Phone No", accessor: "secondaryPhoneNo" }
    ]

    <DataTable
    columnList={columnList}
    dataList={staffList}
    ......../>

where dataList is an array of data, the label field of the columnList object is store the column label, and the accessor is the field name in dataList.

In DataTable component,

export default function DataTable({
  columnList,
  dataList
}) {
return(
<table >
    <thead>
    <tr> {
      columnList.map((column, index) => ( 
      <th> {
          column.label
        } 
      </th>
      ))
    } 
    </tr>
    </thead>      
    <tbody> {
      dataList.map((data) => ( 
        <tr >
        columnList.map((column, colIindex) => ( 
          <td > {
            data[column.accessor]
          } </td>  
        ))
        </tr>
      ));
    } 
    </tbody> 
    </table>
)
}

Upvotes: 0

taolu
taolu

Reputation: 309

You can learn and refer to the "antd" Table Components.The component defined a amount of params to show different format content includes text,icon,image,tree even a table.

Upvotes: 0

Dilshan
Dilshan

Reputation: 3001

Lets think about your requirement. You need to render a table based on given data payload. Table needs to know what's it columns. We can provide columns as a n input as well,

<MyTable
  data={myData}
  columns=['col1', 'col2', 'col3']
/> 

We can assume col1, col2, col3 are keys of the myData object so the MyTable components can now extract the cell data by simply doing myData[columns[i]] where i is the index of column array item.

Based on your mock data, I can see there are nested objects in your data as well. Therefore simple myData[columns[i]] is not going to work. As a solution we can provide a function to return cell value in the component props.

type TableProps<T extends object> = {
  columns: Record<
    string,
    {
      name: string;
      width: string;
      accessor: (data: T) => ReactNode | string | undefined;
    }
  >;
  payload: T[];
};

export const MyTable = <T extends object>(props: TableProps<T>) => {}

As you can see accessor is a function which has one argument type T which returns a React element or string or nothing.

Now in the table component we can simply do,

<TableRow key={index}>
 {Object.keys(columns).map((key) => {
   const { accessor } = columns[key];
   return (
     <TableCell key={key} align="left">
       {accessor(rowData)}
     </TableCell>
   );
 })}
</TableRow>

Then when you use the Table component,

      <TableWithTitle<PayloadType1>
        columns={{
          phone: {
            name: "Phone",
            width: "14%",
            accessor: (payload) => payload.phohe
          },
          notes: {
            name: "Notes",
            width: "14%",
            accessor: (payload) => {
              return payload.notes?.map(({ note }) => note).join(", ");
            }
          }
        }}
      ....

Here is a full code sample

Upvotes: 2

Azzy
Azzy

Reputation: 1731

The basic idea is to make code less fragile to changes, i.e everytime you add a new table type or make changes to existing table, the affect on other table types should be minimum, you can read more about SOLID principles later.

use composition to make component more reusable

There is a basic idea of the solution

// create a factory/config to pick the right header columns based on type
const tableOneColumnHeaders = [
  { id: 1, name: "First name", width: "25%" },
  { id: 2, name: "Second name", width: "16%" },
  { id: 3, name: "Address", width: "14%" },
  { id: 4, name: "Phone", width: "14%" },
  { id: 5, name: "Notes", width: "14%" }
];

const tableTwoColumnHeaders = [
  { id: 1, name: "First name", width: "25%" },
  { id: 2, name: "Status", width: "16%" },
  { id: 3, name: "Author", width: "14%" },
  { id: 4, name: "Date", width: "14%" },
  { id: 5, name: "Media", width: "14%" },
  { id: 6, name: "Rating", width: "14%" },
  { id: 7, name: "Project", width: "14%" },
  { id: 8, name: "", width: "3%" }
];

// poor mans factory
const headerColumnsFactory: headerType = {
  [TableType.FirstTable]: tableOneColumnHeaders,
  [TableType.SecondTable]: tableTwoColumnHeaders

};

// create a row renderer factory/config to pick the right renderer
// each table has a custom body renderer
const TableOneRowsMapper = (props: { data: RowData[] }) => {
 const { data } = props;

  const rows = data as FirtTableDataType[];

  return (
    <>
      {rows?.map((item) => (
        <TableRow key={item.id}>
          <TableCell component="th" scope="row">
            {item.name}
          </TableCell>
          <TableCell align="left">{item.address}</TableCell>
   ...



const TableTwoRowsMapper = (props: { data: RowData[] }) => {
  const { data } = props;

  const rows = data as SecondTableDataType[];

  return (
    <>
      {rows.map((item) => (
        <TableRow key={item.id}>
          <TableCell
            sx={{
              display: "flex",
              direction: "row",
              gap: "5px",
              alignItems: "center"
            }}
          >
            <Avatar />
            {item.who.nickname}
...



const TableBodyRowsComponentFactory = {
  [TableType.FirstTable]: TableOneRowsMapper,
  [TableType.SecondTable]: TableTwoRowsMapper
};

/

/ A component that uses the factories to pick the right renders and render the table

const ExtensibleTable = (props: {
  title: string;
  type: TableType;
  data: any[];
}) => {
  const { title, type, data } = props;

  // if a switch of if is used, this code becomes fragile

  /*
     // with introduction of new if else or modification of existing if
     // othe tables types can break because of shared variable etc
     if (type === '') {
        return  some columsn
     } else if ( type === 'xy') {

     }

 */

  //  but with a bulder the right components are picked
  //  and changes to each type of component are seperated
  //  new ones can be added without affecting this common code
  // pick the right header columns
  const headerColumns: HeaderRowType[] = React.useMemo(
    () => headerColumnsFactory[type] ?? [],
    [type]
  );

  // pick the right row renderer
  const RowRenderer = React.useMemo(
    () => TableBodyRowsComponentFactory[type] ?? TableEmptyRenderer,
    [type]
  );

  return (
    <BaseTable
      title={title}
      headerRow={
        <TableRow>
          {headerColumns.map(({ name, id, width }) => (
            <TableCell align="left" width={width} key={id}>
              {name}
            </TableCell>
          ))}
        </TableRow>
      }
    >
      <RowRenderer data={data} />
    </BaseTable>
  );
};


const BaseTable = (props: IBaseTableProps) => {
  const { title, children, headerRow } = props;

  return (
    <Stack
      gap={"20px"}
      alignItems={"center"}
      sx={{ background: "lightblue", padding: "20px", borderRadius: "20px" }}
    >
      <Typography variant="h3">{title}</Typography>
      <Table>
        <TableHead>{headerRow}</TableHead>
        <TableBody>{children}</TableBody>
      </Table>
    </Stack>
  );
};

I had created a codesandbox example with rest of the example

generally more usable the component becomes, less flexible it becomes

to reduce/handle such issues it helps to apply SOLID principles like Inversion of Control

I am not used to Typescript but I hope helps you in someway and gives you an general idea to make reusable compnents

Upvotes: 0

Related Questions