Adding Meatballs Menu To React-Table Rows

react-table

Meatballs menu (⋯), also called three horizontal dots menu, is a great way of providing contextual options for grid rows. In this article, I will show you how to add the meatballs menu to a table built with @tanstack/react-table.

After reading this article, you will know how to add such a menu to your React app. The end result will look as in the highlighted picture of this article 😉

Creating a table with row selection support

First, let’s define a type of data that we want to display. For our example, we will display a list of Cars:

export type Car = {
id: string;
brand: string;
model: string;
productionYear: number;
isAvailable: boolean;
};
Car.ts

Next, we create a new component called CarsTable, responsible for rendering the table. We will use @tanstack/react-table for the table behavior and react-bootstrap for UI elements.

I implemented the CarsTable component in quite a standard way according to react-table docs, so I won’t copy-paste it here. You can check the whole component’s code here. What’s interesting is that I added support for row selection in a way that makes our table a controlled React component:

type CarsTableProps = {
selectedCar: Car | null;
onCarSelected: (car: Car) => void;
};
export const CarsTable = (props: CarsTableProps) => {
return (
// ...
<tbody>
{table.getRowModel().rows.map((row) => {
const isActive = row.original.id === props.selectedCar?.id;
return (
<tr
key={row.id}
style={isActive === true ? { backgroundColor: "#3a7a11" } : undefined}
onClick={() => {
props.onCarSelected(row.original);
}}
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
);
})}
</tbody>
// ...
)
}
CarsTable.tsx – controlled row selection

As you can see, selectedCar and onCarSelected are managed from outside. Line 14 shows how the row color gets changed for the currently selected car. I recently had to deal with such a case in one of my projects.

So far, so good. This is how it looks, populated with sample data:

react-table table with row selection support

Adding meatballs menu

Ok, we have a table. Now we want to add the meatballs menu. We need a three dots icon and a dropdown menu to open on clicking it.

After quickly going through the react-bootstrap docs, let’s create a new component for that:

import { Dropdown } from "react-bootstrap";
import { Car } from "../types/Car";
import CustomDivToggle from "./CustomDivToggle";
import { BsThreeDots } from "react-icons/bs";

export const CarRowContextMenu = ({ carRow }: { carRow: Car }) => {
return (
<Dropdown key={carRow.id}>
<Dropdown.Toggle as={CustomDivToggle} style={{ cursor: "pointer" }}>
<BsThreeDots />
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item>Option 1</Dropdown.Item>
<Dropdown.Item>Option 2</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
);
};
CarRowContextMenu.tsx

As we want our dropdown toggle to have custom style, I had to provide CustomDivToggle as a custom dropdown component. It’s nothing very interesting, but you can check its implementation here 😉

Next, as we’d like our meatballs menu to be an additional column in the grid, it seems natural to use react-table‘s display column. Let’s try adding it:

columnHelper.display({
id: "context-menu",
cell: (cellContext) => {
const row = cellContext.row.original;
return <CarRowContextMenu carRow={row} />;
},
}),
CarsTable.tsx – adding meatballs menu with display-type column

It looks we have it:

Meatballs menu added to the table with react-tabe's display column

However, after clicking through it for a while, it seems we have an issue. The toggle only opens on every 2nd click:

Meatballs menu with react-table's display column. Double-click issue

Why is that? The reason is our controlled CarsTable component. On clicking a new row, the change event occurs, which triggers the re-render of the CarsTable component (because selectedCar actually changes). It makes react-table re-render the table, with the meatballs menu in its default state (collapsed). On clicking the menu in the same row again, the change event occurs, but the selectedCar does not actually change, which does not trigger the re-render. Initially, it took me a while to figure that out 😀

Fixing double-click issue

In our case, a fix for the double click issue is quite simple. Instead of adding the column with the menu using columnHelper.display() function from react-table, we can render it manually. To do that, we should simply add a new <td> to each row of the table:

<tbody>
{table.getRowModel().rows.map((row) => {
const isActive = row.original.id === props.selectedCar?.id;
return (
<tr
key={row.id}
style={isActive === true ? { backgroundColor: "#3a7a11" } : undefined}
onClick={() => {
props.onCarSelected(row.original);
}}
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
<td>
<CarRowContextMenu carRow={row.original} />
</td>
</tr>
);
})}
</tbody>
CarsTable.tsx – context menu added as a separate <td>

Additionally, to make it work, we need an additional table’s header placeholder:

<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
{/* placeholder header for context menu */}
<th></th>
</tr>
))}
</thead>
CarsTable.tsx – placeholder <th> for context menu

That’s it! Our `react-table` table with row selection support and the meatballs menu works like a charm now:

react-table table with row selection and meatballs menu - final version

Meatballs menu with react-table – source code

You can find the complete source code here. I hope you find it useful! 🙂

GET A FREE GUIDE 🎁

16 STEPS TO BECOME
.NET FULL STACK WEB DEVELOPER
IN 2023

After you sign up, I may be sending you some emails with additional free content from time to time.
No spam, only awesome stuff

.NET full stack web developer & digital nomad
5 1 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments