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 Car
s:
// Car.ts
export type Car = {
id: string;
brand: string;
model: string;
productionYear: number;
isAvailable: boolean;
};
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:
// CarsTable.tsx – controlled row selection
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>
// ...
)
}
As you can see, selectedCar
and onCarSelected
are managed from outside. See settings the style
- this is 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:
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:
// CarRowContextMenu.tsx
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>
);
};
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:
// CarsTable.tsx – adding meatballs menu with display-type column
columnHelper.display({
id: "context-menu",
cell: (cellContext) => {
const row = cellContext.row.original;
return <CarRowContextMenu carRow={row} />;
},
}),
It looks we have it:
However, after clicking through it for a while, it seems we have an issue. The toggle only opens on every 2nd click:
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:
// CarsTable.tsx – context menu added as a separate <td>
<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>
Additionally, to make it work, we need an additional table’s header placeholder:
// CarsTable.tsx – placeholder <th> for context menu
<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>
That’s it! Our react-table
table with row selection support and the meatballs menu works like a charm now:
Meatballs menu with react-table – source code
You can find the complete source code here. I hope you find it useful! 🙂
Top comments (0)