This post is a continuation of my article on Using React Query with React Table. I recommend reading that first to understand the full context.

We will also be following along with the same set of example code which can be found at github.com/nafeu/react-query-table-sandbox. We will be focusing on the ReactTableExpanding.jsx file.

You can get it quickly set up with:

git clone https://github.com/nafeu/react-query-table-sandbox.git
cd react-query-table-sandbox
npm install
npm start

The Expandable Table We Want To Build

Understanding Our Data

[
{ id: 1, name: 'foo' },
{ id: 2, name: 'bar' },
{ id: 3, name: 'baz' }
]

And in a table would render:

But what if we had more of a hierarchical (or tree-like) structure such as:

[
{
id: 1,
name: 'foo'
children: [
{ id: 4, name: 'qux' },
{ id: 5, name: 'quux' }
]
},
{ id: 2, name: 'bar' },
{ id: 3, name: 'baz' }
]

This is where things get wonky and we have to shift our approach a tiny bit. Let’s say we want to render this, but only render the children when we click on an individual row. For example, if we were to click on the row of id: 1, we would want to see a flat table rendered like:

In the real world, we have many instances where this is the case and they come in the form of drill-down tables. This is helpful for BI dashboards, video game leaderboards, accounting, or pretty much any case where you need to further investigate a single row in a table.

In index.mjs from our example code, we have defined an API that returns mock data about an imaginary software dev discussion group.

The api/ endpoint returns data in the form of:

[
{
"id": 1,
"name": "How to use react-table in reporting dashboard",
"active": 40,
"status": "locked",
"upvotes": 30
},
{
"id": 2,
"name": "How to use react-query for BI solution",
"active": 31,
"status": "resolved",
"upvotes": 39
}
...
]

The api/child endpoint returns similar data but provides new entries every time, whereas the root api/ endpoint returns the original payload with slight modifications. This is to simulate an "active website" where data changes in pseudo real-time.

Although the example is a bit rough, imagine for a moment that each of these discussion topics have sub-issues which are structured in the same way. These will be used to persist a “hierarchical” structure in our table.

In our front-end code, we have:

const fetchParentData = () => axios.get(`http://localhost:8000/api`);
const fetchChildData = () => axios.get(`http://localhost:8000/api/child`);

Which are promise-based API helpers that retrieve the partially dynamic parent data and novel set of child data respectively (it is required for React Query to use promise-based API helpers).

Recursively Modifying Row Data

row-0
row-1
row-2

to

row-0
row-0.0
row-0.1
row-1
row-2

and even further to

row-0
row-0.0
row-0.0.0
row-0.0.1
row-0.1
row-2
row-3

and so on and so forth. In our example code, we do this using the recursivelyUpdateTable function which is as follows:

Here we take:

  • tableData -> existingRows which is our existing table data
  • childData -> subRowsToInsert which represents the new rows we want to insert
  • id -> path which is formatted as x.y.z and split into [x, y, z] where x, y and z can be indices representing specific rows in a collection and the entire id effectively serves as a path to a specific node (row),

We use these values and some basic knowledge of recursive algorithms to first grab our current path for traversal:

const id = path[0];

Then we formalize our base case (which in our case is when there just one path index left), decide if the node that we have reached has sub-rows or not, and then insert the row:

If we are not in our base case, we recursively use the function insertIntoTable and feed in a subset version of our current rows:

This may be a little complicated to get, especially if you haven’t written much recursive code in a while, so feel free to take your time to trace through and understand it.

The purpose of this article isn’t to focus on recursion but instead to focus on using React Table to pull off the lazy-loading expandable rows. This helper however is still a very important part of it.

Building On Our React Query + React Table Proposal

The proposal consists of structuring a query layer, data processing layer and rendering layer between the three components as follows:

TableQuery
TableInstance
TableLayout

Let’s build on top or expand on this example, refer now to ReactTableExpanding

Updating our TableQuery Component

It should be noted that isRowLoading is a loading state variable. It is a mapping of paths to a boolean value, that helps us keep track of which row is currently in the process of fetching data:

We also use recursivelyUpdateTable to modify our existing table data with the child data that has been received from our API, this is why our recursive function is so important. One of the beauties of React Table is that it already knows how to work with embedded structures, all you need to do is provide them using a subRows property inside an individual row and it takes care of the rest.

We also have this invocation of useQuery that we modify:

const {
data: apiResponse,
isLoading
} = useQuery('discussionGroups', fetchParentData, { enabled: !tableData });

We add enabled: !tableData to prevent the table form re-fetching in the background as it can mess up the embedded structure. Implementing re-fetching with an embedded structure is a more advanced topic that won't be covered in this article.

We also pass some more props into the TableInstance like so:

return (
<TableInstance
tableData={tableData}
onClickRow={handleClickRow}
isRowLoading={isRowLoading}
/>
);

Updating our TableInstance Component

Here is where some of the magic happens. We use the Cell property to instruct React Table on how it should format an individual cell, in this instance, we add an additional column in front of our table which will contain the expander button and we also add some additional logic to trigger our custom onClickRow handler if the row is in the appropriate loading and expanding state.

You may be wondering, where did { row, isLoading, isExpanding } come from? The row object is actually something that React Table provides for us to access when using the Cell property, this is super helpful for us as row also has a useful method called getToggleRowExpandedProps() which we can use to manually trigger React Table's row expansion functionality.

We also add the useExpanded hook in our table instance instantiation with the option autoResetExpanded set to false:

const tableInstance = useTable(
{ columns, data, autoResetExpanded: false },
useExpanded
);

And we pass the isRowLoading object into the TableLayout:

return (
<TableLayout
{...tableInstance}
isRowLoading={isRowLoading}
/>
);

Updating our TableLayout Component

const TableLayout = ({
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
isRowLoading,
state: { expanded }
}) => {
...
}

The main update is in our individual row rendering:

return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>
{cell.render('Cell', {
isLoading: isRowLoading[row.id],
isExpanded: expanded[row.id]
})}
</td>
})}
</tr>
)

In the cell.render method, we add an extra object that is filled with:

{
isLoading: isRowLoading[row.id],
isExpanded: expanded[row.id]
}

This works in tandem with our previously declared code in the TableInstance component where we are able to feed custom values directly into a Cell. We defined isRowLoading -> isLoading ourselves and are feeding expanded -> isExpanded to enforce loading state and expanded state. Doing so isn't even that hard to read, that is what makes React Table so incredible to work with.

Once you’ve got all of this put together, you’ve got yourself this fancy table:

Happy Coding!

is a musician and full-stack web developer from Toronto, Canada who studied Computer Science and Linguistics at the University of Toronto.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store