4 min read

Strongly Typed React Table Component

Share this article!
TwitterLinkedInFacebookPocketBufferEmail
This post is part of an ongoing series that highlights the technologies and products being built by the Atrium EPD team.

 

After implementing a few tables within Atrium’s web app, we decided we had reason to make a generic table component to speed up development time in the future.

Much of our existing code looked like this:

function existingTable(data: Array<RowData>) {
  return (
    <table>
        <th>
          <td>Name</td>
          <td>Status</td>
          <td>Link</td>
        </th>
     {data.map((dataRow: RowData) => (
        <tr>
          <td>dataRow.name</td>
          <td>dataRow.status</td>
          <td><a href={`/datum/${dataRow.id}`}>Visit</a></td>
         </tr>
      )}
    </table>
  )
}

Our existing components have explicit types which provide strong type guarantees. They weren’t very difficult to copy and paste, but they were tedious with an HTML tag for every row and cell.

We thought that someone must have solved this problem, and many people have. There are a few all-bells-and-whistles-included react table components. Some were overwhelming to look at with 20 or more parameters available and some were more elegantly coded.

I was unable to find a 3rd party table component that provides strong typing however. Most are written in JS, some are javascript with typescript annotations for exported code, and the rest use `any` and therefore essentially have untyped APIs. We already had very strong types and I did not want to give that up.

 

How to use

Our first step to implementing a new common component was to propose an example usage. After a few iterations, we wound up with something like this:

import Table, { ColumnDescription } from './Table'

type ExampleRowType = {
  id: number
  name: string
  dayOfArrival: string
}

const EXAMPLE_DATA: ExampleRowType = [
  {
    id: 1,
    name: 'Verla Bashirian',
    dayOfArrival: '1/2/18'
  },
  {
    id: 2,
    name: 'Daniella Mante',
    dayOfArrival: '2/16/17'
  },
  {
    id: 3,
    name: 'Estrella Feil',
    dayOfArrival: '5/12/18'
  },
]

const COLUMN_DESCRIPTIONS: Array<ColumnDescription<ExampleRowType>> = [
  {
    key: 'dayOfArrival',
    format: (row: ExampleRowType): string => row.dayOfArrival.replace('/', ' ')
  },
  {
    key: 'name',
    title: 'Applicant Name'
  },
  {
    key: actionDelete,
    title: 'Delete',
    format: (row: ExampleRowType): React.ReactNode => {
      return <button onClick={() => mockedButtonCallback(row.id)}>Delete</button>
    }
  }
]

renderTable() => (
  <Table
     data: EXAMPLE_DATA,
     columns: COLUMN_DESCRIPTIONS,
   />
)

The important type in all of this is ColumnDescription<DataType>, which describes how to display a column.

ColumnDescription is composed of either a ColumnDescriptor, which must have a key attribute that corresponds to a key in the RowDataType, or a OtherColumnDescriptor, which has a key that is explicitly not a key in RowDataType.

export type ColumnDescription<RowDataType> =
  | ColumnDescriptor<RowDataType, keyof RowDataType>
  | OtherColumnDescriptor<RowDataType, Exclude<string, keyof RowDataType>>

export type ColumnDescriptor<RowDataType, AttributeKey extends keyof RowDataType> = {
  key: AttributeKey
  title?: string | React.ReactNode
  tdClassName?: string
  format?: (value: RowDataType) => React.ReactText | React.ReactNode
}

export type OtherColumnDescriptor<RowDataType, OtherColumnKeys> = {
  key: OtherColumnKeys
  title?: string | React.ReactNode
  tdClassName?: string
  format: (value: RowDataType) => React.ReactText | React.ReactNode
}

The table component iterates over the data provided to render the rows and for each row it iterates over the list of columns to render each cell. When key is not a key in RowDataType, there is no data for the table component to display. The typescript type OtherColumnDescriptor requires a format function to remind the programmer to provide something to display.

 

Design Decisions

 

How Strongly Typed Can It Be?

We went into this project against the idea that an `any` type would be helpful in the long term. Typescript and its DefinitivelyTyped libraries have very strong guarantees that I’ve come to value and I don’t want to compromise them.

When at all possible, Typescript should guard against incorrect use of our own code.

I understand that interfaces are more strongly typed than Typescript types but we haven’t used them extensively in our codebase so our Table component wound up not using interfaces at all.

 

Use The Native Table Component

Tables have a bad rep for being used as a layout tool rather than just for tabular data. We have tabular data though, so let’s double down on that and use HTML’s table component.

By using the HTML table component we get automatic column widths for free without any extra javascript. We also get easy border collapsing.

This same type pattern could be used for a list if the need arises. That would trade automatic column widths for proper display of non-tabular data.

 

How Extensible Should It Be?

I’ve always found that the answer to this question is “As extensible as it can be without being overwhelming”. It’s nice to achieve as extensive customizations as the underlying technology used though. We opted for “As extensible as we need it to be” with the imperative that any flexibility mirror the HTML table component.

In our current implementation we wound up with a few methods of extensibility:

  • The property tableClassName is inserted into the table component’s className
  • The property trClassName is used to compute the row’s className from the row’s data
  • The property trAttributes can be used to set any valid <tr> attribute
  • ColumnDescription can declare a tdClassName for the cell’s classname
  • ColumnDescription can provide a function that produces a ReactNode as the content of the cell
  • ColumnDescription can provide a ReactNode as the header of a column

The level of extensibility varies based on the need we felt at the time. I’ve been happy with just using tableClassName, trClassName, and ColumnDescription’s render attribute to change the format of the table.

Typescript could let us consolidate ColumnDescription’s tdClassName and format attributes into one `format: (value: RowDataType) => HTMLTableCellElement` but we didn’t notice that right off the bat and nobody’s felt the need to implement it just yet.

 

Publishing

Atrium has published this code at github licensed under the MIT licence with thanks to Atrium Legal Technology Services who is giving me time to maintain the repo.

I’m publishing this as a pattern for others to follow in their own codebase rather than publishing it as an NPM module which would take more time to maintain than I’m currently able to commit to.

 

Contributing

I’m currently accepting PRs and tracking issues for:
Bugs in the code
Weak types that could be strengthened
Simplifications to the type signatures or to the API that do not compromise the extensibility of the code
Additions to the `EXAMPLES.md` file with other github repos that use this component

Share this article!
TwitterLinkedInFacebookPocketBufferEmail
mm

Software Engineer at Atrium