Skip to main content

Grouping, filtering and aggregation in Tanstack table

·1942 words·10 mins
Tanstack-Table React
Table of Contents

TLDR; Grouping, aggregation, filtering, pagination of data in tanstack table without user input

In modern web applications, data often needs to be presented in multiple views or perspectives based on specific requirements or user preferences. These different views can be achieved through filtering, grouping, or aggregating the underlying data. However, there are scenarios where we want to display these multiple views without explicit user input or interaction.

For example, consider an e-commerce website that displays product listings. On the main page, we might want to show a condensed view with products grouped by category, while on the product category page, we want to display an expanded view with detailed product information and filtered by specific criteria. In such cases, we need a way to manage and present the data in different forms without relying on user input.

This is where a powerful data-grid library like Tanstack Table comes into play. With Tanstack Table, we can define and control these operations programmatically, enabling us to present multiple views of the same underlying data without requiring user input.

In this article, we'll explore how to leverage Tanstack Table's filtering, grouping, and pagination capabilities to achieve this goal. We'll cover the problem statement in depth, explain the relevant concepts, and provide code examples to illustrate the implementation.

By the end of this article, you'll have a solid understanding of how to use Tanstack Table to create dynamic and efficient data representations tailored to your application's needs, without relying on user interactions.

Filtering in Tanstack table

Let's consider a sample dataset of products that we'll use throughout our examples:

const data = [
  { id: 1, name: 'Product A', category: 'Electronics', price: 99.99 },
  { id: 2, name: 'Product B', category: 'Clothing', price: 29.99 },
  { id: 3, name: 'Product C', category: '', price: 14.99 },
  { id: 4, name: 'Product D', category: 'Electronics', price: 49.99 },
  // ... more products
];

Filtering data is the process of selecting a subset of records from a larger dataset based on specific criteria or conditions. In the context of tables, filtering allows us to present a view that displays only the rows that match certain criteria, effectively hiding the rows that don't meet those criteria.

Tanstack Table provides a powerful filtering mechanism that can be applied at the column level. By defining a `filterFn` for a column, we can specify the logic for determining which rows should be included or excluded from the filtered view.

Adding filtering to tanstack table requires 3 things

  • Defining filterFn that will be used to filter values from a column
  • Importing getFilteredRowModel and initializing getFilteredRowModel while creating table instance
  • Adding state to tanstack table that specifies which columns need to be filtered.

Here's an example of how we can define column filters using Tanstack Table:

const columns = [
  // Other column definitions...
  {
    id: 'category',
    accessorKey: 'category',
    header: 'Category',
    filterFn: (row, columnId, filterValue) => {
      // Filter out products with an empty category
      return row.getValue(columnId) !== '';
    }
  },
  // More column definitions...
];

Typically, the `filterValue` argument in the `filterFn` is provided by the user via a UI input. However, in our case, where we want to filter without user input, we can ignore this value and instead filter based on pre-existing criteria, such as filtering out products with an empty category.

State can be added as shown below

// Other table related code
const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  getFilteredRowModel: getFilteredRowModel(),
  state: {
    columnFilters: [{id:'catgory', value: ''}],
  },
});
// Rendering table code

While defining filters individually for each column works, it's often more efficient and scalable to write a generic function that combines the `filterFn` definitions to add filters. This also keeps table UI component reusable as we don't have to specify columnid every time we want to add a filter to it.

const initialColumnFilters = React.useMemo(
  () =>
    columns.filter((column) => column.filterFn).map((column) => ({
      id: column.id,
      value: '',
    })),
  [columns]
);

In this code snippet, we use the `useMemo` hook to memoize the computation of `initialColumnFilters`. This ensures that the filters are only recalculated when the `columns` array changes, providing better performance and preventing unnecessary re-computations.

The `initialColumnFilters` array is created by iterating over the `columns` array and selecting only the columns that have a `filterFn` defined. For each column with a `filterFn`, it creates an object with the column ID and empty string as the `value`.

Here is an example showing how to use the state

const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  getFilteredRowModel: getFilteredRowModel(),
  state: {
    columnFilters: initialColumnFilters,
  },
});

With this approach, we can apply the pre-defined column filters by providing the `initialColumnFilters` array as part of the table state. By using this generic approach, we can filter the table data based on pre-defined criteria without requiring user input. The `getFilteredRowModel` function from Tanstack Table will then apply these filters to the table rows, presenting a filtered view of the data based on the specified criteria, without requiring user input.

Grouping and Aggregation in Tanstack table

Let's consider the same sample dataset of products that we used in the previous section:

const data = [
  { id: 1, name: 'Product A', category: 'Electronics', price: 99.99 },
  { id: 2, name: 'Product B', category: 'Clothing', price: 29.99 },
  { id: 3, name: 'Product C', category: '', price: 14.99 },
  { id: 4, name: 'Product D', category: 'Electronics', price: 49.99 },
  // ... more products
];

Grouping data is the process of organizing records into logical groups based on one or more criteria or columns.

Adding grouping to tanstack table requires 4 things

  • Making `enableGrouping` to indicate the column is available for grouping.
  • Defining accessorFn that will be used to group values from a column or accessorKey if column can be directly grouped without pre-processing. For e.g. if 'category' is accessorKey in data than multiple 'Electronics' values will be grouped together. If however you want 'Product A' and 'Product B' in one group than you will have to write a accessorFn that returns same value for 'Product A' and 'Product B'.
  • Importing getGroupedRowModel and initializing getGroupedRowModel while creating table instance
  • Adding state to tanstack table that specifies which columns need to be grouped.

Here is the example of grouping products based on their category.

const columns = [
  // Other column definitions...
  {
    id: 'category',
    accessorKey: 'category',
    header: 'Category',
    enableGrouping: true, // Enable grouping for this column
  },
  // More column definitions...
];

State can be added as shown below

// Other table related code
const table = useReactTable({
  data,
  columns,
  getCoreRowModel: getCoreRowModel(),
  getGroupedRowModel: getGroupedRowModel(),
  state: {
    columnFilters: [{id:'category'}],
  },
});
// Rendering table code

Similar to specifying filter state we can take advantage of 'enableGrouping' property of column to infer that the column needs to be grouped and set the state for that column.

const initialGroupFilters = React.useMemo(
  () =>
    columns
      .filter((column) => column.enableGrouping)
      .map((column) => column.id),
  [columns]
);

This code snippet creates an array of initial group filters by iterating over the `columns` array and selecting only the columns that have `enableGrouping` set to `true`. For each column with `enableGrouping` enabled, it adds the column ID to the `initialGroupFilters` array.

The `initialGroupFilters` array needs to be specified in the table state when initializing the table:

const table = useReactTable({
  data,
  columns,
  enableGrouping: true,
  getGroupedRowModel: getGroupedRowModel(),
  // ... other model functions
  state: {
    grouping: initialGroupFilters,
    // ... other state properties
  },
});

When a column is grouped, Tanstack Table provides the `table.getGroupedRowModel().rows` array, which contains the entries after grouping. This array can be used to perform further operations or calculations on the grouped data. For example, if you want to find the percentage contribution of each grouped value, you can use `table.getGroupedRowModel().rows` to access the grouped data and perform the necessary calculations.

Aggregation

Aggregation generally goes hand in hand with grouped columns. To aggregate data you can define an `aggregationFn` to display aggregated values for each group. For example, we can show the average price of products in each category group:

const columns = [
  // Other column definitions...
  {
    accessorKey: 'price',
    header: 'Price',
    aggregationFn: 'mean'
    aggregatedCell: (cell) => {return (<div>{cell.getValue()}</div>)}
  },
  // More column definitions...
];

In this example, we define a column with the `accessorKey` of 'price'. The `aggregationFn` calculates the average of all values in the group, and the `aggregatedCell` renders the average price with a custom format. Here 'mean' is a built-in function provided by tanstack table for averaging values in a group. Other built-ins like 'sum', 'max', 'median' are also available. You can also define your own aggregation function if built-in functions are not sufficient for your requirements.

One thing to note here is to display aggregatedCell, you need to make sure that your rendering logic is taking care of rendering aggregatedCells and not just "normal" Cells, like this

{cell.getIsAggregated() ? (
	    flexRender(cell.column.columnDef.aggregatedCell, cell.getContext())
	  ) : (
	    flexRender(cell.column.columnDef.cell, cell.getContext())
)}

Conditional Pagination in Tanstack Table

When working with large datasets, it is often necessary to split the data into multiple pages to improve performance and enhance the user experience. Pagination allows users to navigate through the data in a more organized and manageable way, reducing the amount of information displayed at once and making it easier to find specific records.

Tanstack Table provides built-in support for pagination, allowing you to easily implement this functionality in your tables. However, in our scenario, where we want to display multiple views of the same data without user input, we can leverage Tanstack Table's pagination capabilities to conditionally render the pagination component based on the number of pages available.

First, let's define the necessary column definitions for our sample data:

const columns = [
  // Column definitions...
];

Next, we need to initialize the table instance with the `getPaginationRowModel` function:

const table = useReactTable({
  data,
  columns,
  getPaginationRowModel: getPaginationRowModel(), // Enable pagination
  // Other model functions...
});

By including `getPaginationRowModel` in the table initialization, we enable pagination functionality for our table.

To conditionally render the pagination component, we can check the number of pages available using `table.getPageCount()`. If the number of pages is greater than 1, it means we have multiple pages, and we should display the pagination component:

{table.getPageCount() > 1 && (
  <div>
    <button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
      Previous
    </button>
    <span>
      Page{' '}
      <strong>
        {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
      </strong>
    </span>
    <button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
      Next
    </button>
  </div>
)}

In the code above, we first check if `table.getPageCount()` is greater than 1. If this condition is true, it means we have multiple pages, and we render the pagination component. The pagination component consists of a "Previous" button, a page indicator, and a "Next" button.

The "Previous" button is rendered with an `onClick` handler that calls `table.previousPage()`, which navigates to the previous page. We also use `table.getCanPreviousPage()` to disable the button if there is no previous page available.

The page indicator displays the current page index (`table.getState().pagination.pageIndex + 1`) and the total number of pages (`table.getPageCount()`).

Similarly, the "Next" button is rendered with an `onClick` handler that calls `table.nextPage()`, which navigates to the next page. We use `table.getCanNextPage()` to disable the button if there is no next page available.

By conditionally rendering the pagination component based on the number of pages, we can ensure that the pagination controls are only displayed when necessary, providing a clean and efficient user interface without requiring user input to initiate pagination.

Conclusion

By combining Tanstack Table's powerful features with generic approaches to define filters, grouping, and other operations, we can programmatically control and present multiple views of the same underlying data, tailored to our application's needs, without relying on user interactions. This flexibility can significantly enhance the user experience and provide valuable insights into the data.

Related

How to add custom styling in tanstack react table
··1983 words·10 mins
Tanstack-Table React
Million dollars is not cool, you know what is cool? million rows
·1922 words·10 mins
Csv React Table Js
CSV Viewer with charts
··1394 words·7 mins
Csv Chart Tool
All in one dashboard for google analytics and search console plan & execution
··2265 words·11 mins
Indiehacker
How to count rows read (scanned) in sqlite
··896 words·5 mins
Sqlite Cloudflare D1 Turso
Comparison of managed sqlite services
··1607 words·8 mins
Sqlite Cloudflare D1 Turso