DEV Community

Cover image for Adding Meatballs Menu To React-Table Rows
Dawid Sibiński
Dawid Sibiński

Posted on • Originally published at codejourney.net on

Adding Meatballs Menu To React-Table Rows

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:

// Car.ts
export type Car = {
  id: string;
  brand: string;
  model: string;
  productionYear: number;
  isAvailable: boolean;
};
Enter fullscreen mode Exit fullscreen mode

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>
    // ...
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

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:

// 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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} />;
      },
    }),
Enter fullscreen mode Exit fullscreen mode

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:

// 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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! 🙂

Top comments (0)