Creating a table with zustand


Intro: What’s a table?

Tables are complex structures. You’re able to sort data, filter data, reformat and reorganize rows, maybe even hide columns and edit formatted cell data. For situations where you want to display tabular data, solutions like simply looping through a 2d-array displaying a small single-page table might be the sufficient way to go:

See the Pen Untitled by Jonathan Hassel (@jotoh98) on CodePen.

But for higher-level features like sorting or filtering the situation changes. Now you’re concerned about an internal data model, pagination, sort and filter inputs, cell output formatting… The list seems to go on and on. Here I want to cover the basics of a table component structure flexible enough to tackle each problem after another. To complete this, you need some basic knowledge of React and hooks.


Data flow in dynamic tables

Let’s start with the basic data flow. To show a current slice of data we can look at two factors limiting the scope in which data is visible: columns and rows.

Column Filtering

Limiting columns is pretty straightforward. For each row that we render (1), we loop over the indices of the columns (2) that we want to show.

Notice how we now have to “abstract” away from the actual data accessing (calling cell) and instead get the text to render via an index access (3).

See the Pen Simple Table 1 by Jonathan Hassel (@jotoh98) on CodePen.

Row Filtering

Limiting rows is a bit more complicated. We need to know the current page and the number of rows per page. With that information we’re able to calculate the start and end index of the rows we want to show. The workflow is as follows:

Table preprocessing

  1. First, we have the full data set that we filter for several criteria. In most cases you want to search for a query but sometimes a date range or a greater-than might be a possible filtering scenario. A simple query filter predicate would look like this:
const filterPredicate = (cellData, query) => cellData.includes(query);
  1. After filtering we might want to sort the data. In many languages (including js) the sorting functionality is realised by taking two element of a collection (in JS an array) and calculating a comparison number. A sorting function sorting an array in ascending order might look like this:
const sortNumbers = (a, b) => a - b;

That’s it, now we might want to sort other data as well, so for each column that we want to sort we need an according sort function. 3. Finally, we cut the filtered and sorted data in chunks, so-called pages. By doing this we’re able to display only a fraction of the big amount of data at a time and navigate this data with a pagination.

You might be wondering: “Why filter first and sort afterwards?“. You could indeed achieve the same result by switching the two operations.

Let’s look at the runtime. Filtering is a process taking place in O(n)-time, meaning: With n rows you only need to look at each of them once resulting in n decisions/actions taking place.

Sorting on the other hand has a time complexity of O(n log n). By filtering first we reduce the number of rows before passing it to the sort algorithm, why sort more elements than necessary?


Short Intro to Zustand

Zustand is a state management library reducing boilerplate code immensely by making classic reducers obsolete. Like redux, you work with one central state, but unlike redux, you can work with this state much more loosely than incorporating it at a central place in your React application. For a quick and easy demo please visit their cute homepage.

What’s inside our store?

Essential for our table here are their concept of state and methods to alter that state. Let’s begin by collecting the state’s properties. What do we need?

Data Properties

User Input Properties

Moving away from data controlled properties, we enter direct user input:

Last but not least we have some configuration properties related to the data we’re translating to table rows.

Let’s say for example we wanted a row entity called Customer to work with data from our online shop database. The customer type might look something like this:

type Customer = {
  firstname: string;
  lastname: string;
  address: {
    street: string;
    streetNo: string;
    city: string;
    postcode: string;
  };
  orders: number;
  refunds: number;
};

Notice how we spiced things up by nesting the address in there? We’ll even be able to work with any sub-structures.