Ibadehin Mojeed I'm an advocate of project-based learning. I also write technical content around web development.

Creating a React sortable table

9 min read 2709

Creating A React Sortable Table

Sometimes when we create a table to present data in our application, we may want to add a sorting functionality for data management.

While there are libraries like the React Table that allow us to add a sorting functionality and more, sometimes using libraries is not the best fit, especially if we want a simple table with total flexibility.

In this tutorial, we will cover how to create a sortable table with React from scratch. We will sort table rows in ascending or descending order by clicking on the table headers.

In addition, we’ll learn how to properly use the JavaScript sort() function and some important React principles. At the end of this tutorial, we will have a working sortable table.

This is what our finalized project looks like. You can interact with it, and after that, get started!

To follow this tutorial, you must have a working knowledge of React.

Creating the table markup in React

Let’s start by creating a React project with create-react-app and start the development server. Once the project is up and running, we will create the table markup.

Recall from HTML, the table markup follows the following structure that includes the table caption:

<table>
  <caption>Caption here</caption>
  <thead>
    <tr>
      <th>{/* ... */}</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>{/* ... */}</td>
    </tr>
  </tbody>
</table>

Since React is a component-based library, we can split the markup into different component files. The Table component will serve as the parent component holding the TableHead and TableBody components.

Inside these children components, we will render the table heading and the body contents respectively. And finally, we will render the parent component inside the App component.

We made a custom demo for .
No really. Click here to check it out.

In the src folder, let’s create the files so we have the following structure:

react-sortable-table
   ...
    ├── src
    │    ├── components
    │    │      ├── Table.js
    │    │      ├── TableBody.js
    │    │      └── TableHead.js      
    │    ├── images
    │    ├── App.js
    │    ├── index.css
    │    └── index.js

Notice we also added an images folder in the src. This will hold the icons indicating the direction of sorting. Let’s get the icons from the project here and add them to the src/images folder.

Next, open the src/index.js and update the file so we have:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(
 <React.StrictMode>
  <App />
 </React.StrictMode>,
 document.getElementById('root')
);

And then, update the src/App.js file so we have the following:

const App = () => {
 return <>App</>;
};

export default App;

Before we save the files, let’s get the CSS for the project here to replace the src/index.css file. If we now save the files, we should see a simple “App” text rendered in the frontend.

Getting the table’s data

Usually when we work with tables, we get the table’s data from an API or a backend server asynchronously. However, for this tutorial, we will generate some mock but realistic data from Mockaroo and get the returned JSON data that looks like this:

[
 {
  "id": 1,
  "full_name": "Wendall Gripton",
  "email": "[email protected]",
  "gender": "Male",
  "age": 100,
  "start_date": "2022-01-26"
 },
 // ...
]

So, let’s create a data.json in the src folder, copy the data from the project file here, and paste it into the file we just created. Now, save the file.

In the file, you’ll notice we added some null values to represent the missing values. This was intentional to show how to properly sort values of the null data type.

Rendering the table data

In the components/Table.js file, let’s start by adding the following code:

import { useState } from "react";
import mockdata from "../data.json";
import TableBody from "./TableBody";
import TableHead from "./TableHead";

const Table = () => {
 const [tableData, setTableData] = useState(mockdata);

 const columns = [
  { label: "Full Name", accessor: "full_name", sortable: true },
  { label: "Email", accessor: "email", sortable: false },
  { label: "Gender", accessor: "gender", sortable: true },
  { label: "Age", accessor: "age", sortable: true },
  { label: "Start date", accessor: "start_date", sortable: true },
 ];

 return (
  <>
   <table className="table">
    <caption>
     Developers currently enrolled in this course, column headers are
     sortable.
    </caption>
    <TableHead columns={columns} />
    <TableBody columns={columns} tableData={tableData} />
   </table>
  </>
 );
};

export default Table;

The code is self-explanatory. We imported the table data and stored it in the state. Then, we passed it to the TableBody component via the prop. We also defined the table headers as an array of objects and assigned them to the columns variable.

We can now loop through the variable in the TableHead component to display the table headers and also use the accessor keys to dynamically access and display the body row data.

For this, the accessor must match the data keys in the data.json file. The sortable key used in the columns allows us to enable or disable sorting for a particular column.

Moving forward, let’s access the data in the children’s components so we can render them. Let’s add the following code in the components/TableHead.js file:

const TableHead = ({ columns }) => {
 return (
  <thead>
   <tr>
    {columns.map(({ label, accessor, sortable }) => {
     return <th key={accessor}>{label}</th>;
    })}
   </tr>
  </thead>
 );
};

export default TableHead;

Next, let’s add the following in the components/TableBody.js file:

const TableBody = ({ tableData, columns }) => {
 return (
  <tbody>
   {tableData.map((data) => {
    return (
     <tr key={data.id}>
      {columns.map(({ accessor }) => {
       const tData = data[accessor] ? data[accessor] : "——";
       return <td key={accessor}>{tData}</td>;
      })}
     </tr>
    );
   })}
  </tbody>
 );
};

export default TableBody;

Finally, update the src/App.js file to include the Table component:

import Table from "./components/Table";

const App = () => {
 return (
  <div className="table_container">
   <h1>Sortable table with React</h1>
   <Table />
  </div>
 );
};

export default App;

Let’s save all files and check the frontend. We should see our table rendered.

Rendered Table In Frontend

Sorting the React table data

Now, whenever we click any of the table headers, we can sort that particular column in ascending or descending order. To achieve this, we must use an ordering function that knows how to collate and order items. In this case, we will use the sort() function.

However, depending on the item’s data type, we can sort elements using different methods. Let’s take a look at that real quick.

The basic sort() function

In the simplest form, we can use the sort() function to arrange the elements in the arr array:

const arr = [3, 9, 6, 1];

arr.sort((a, b) => a - b);
console.log(arr); // [1, 3, 6, 9]

The sort(), through its algorithm, knows how to compare its elements. By default, it sorts in ascending order. The above syntax works if the sort items are numbers. For strings, we have something like this:

const arr2 = ["z", "a", "b", "c"];

arr2.sort((a, b) => (a < b ? -1 : 1));
console.log(arr2); // ["a", "b", "c", "z"]

Here, the sort() compares items and returns an integer to know if an item is moved up or down in the list. In the above implementation, if the compare function returns a negative number, the first item, a, is less than b, and therefore moved up, which indicate ascending order and vice versa.

Understanding the sorting principle with the sort() function is vital to ordering table data. Now, let’s see more examples.

If we sort the following data by name:

const data = [
  { name: "Ibas", age: 100 },
  {
    name: "doe",
    age: 36
  }
];

We will have the following code:

const data1 = [...data].sort((a, b) => (a.name < b.name ? -1 : 1));

data1.map((d) => console.log("without conversion", d.name)); // Ibas, doe

Notice the output is not what we expect. We expect doe to be listed before Ibas based on the default ascending rules. Well, this happens because characters are sorted by their Unicode values.

In the Unicode table, capital letters have a lesser value than small letters. To ensure we see the expected result, we must sort by case-insensitive by converting the sort items to lower cases or upper cases.

Our code should now look like so:

const data2 = [...data].sort((a, b) =>
  a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1
);

data2.map((d) => console.log("with conversion", d.name)); // doe, Ibas

This works as expected. However, in our project, we will sort the table headers by different data types, which includes number, string and date.

In the above implementation, we cannot pass a number because the .toLowerCase() function only exists on strings. This is where the localeCompare() function comes in.

Using localeCompare() with the sort() function

This function is capable of handling different data types including strings in different languages so they appear in the right order. This is perfect for our use case.

If we use it to sort our data array by name, we have the following:

const data3 = [...data].sort((a, b) => a.name.localeCompare(b.name));

data3.map((d) => console.log("with localeCompare", d.name, d.age)); // doe 36, Ibas 100

Like the earlier compare function, the localeCompare() also returns a number. In the above implementation, it returns a negative number if the a.name is less than b.name and vice versa. This function is only applied on a string, but it provides an option for numeric sorting.

Back to our data array, we can sort by age number by calling .toString() on age to get a string representation:

const data4 = [...data].sort((a, b) =>
  a.age.toString().localeCompare(b.age.toString())
);

data4.map((d) => console.log("with localeCompare", d.name, d.age)); // ibas 100, doe 36

Again, in the code we notice that 100 is coming before 36 which is not what we expect. This also happens because the values are strings and hence, "100" < "36" is correct. For numeric sorting, we must specify the numeric option, so we have:

const data5 = [...data].sort((a, b) =>
  a.age.toString().localeCompare(b.age.toString(), "en", {
    numeric: true
  })
);

data5.map((d) => console.log("with localeCompare", d.name, d.age)); /// doe 36, Ibas 100

As seen above, we are now getting the proper arrangement. In the code, we also included an optional "en" locale to specify the application language.

Now that we’ve refreshed how to use the sort() function, implementing it in our project will be a piece of cake.

Handling the onClick event and sorting data

When we click a particular table header, we must keep track of the sort order and the sort column. For this, we must use the useState Hook.

In the components/TableHead.js, import the useState Hook and use it like so:

import { useState } from "react";

const TableHead = ({ columns }) => {
 const [sortField, setSortField] = useState("");
 const [order, setOrder] = useState("asc");
 return (
  // ...
 );
};

export default TableHead;

Next, add an onClick event to the table header, th, and its handler function above the return statement:

const TableHead = ({ columns }) => {
// ...

 const handleSortingChange = (accessor) => {
  console.log(accessor);
 };

 return (
  <thead>
   <tr>
    {columns.map(({ label, accessor, sortable }) => {
     return (
      <th
       key={accessor}
       onClick={sortable ? () => handleSortingChange(accessor) : null}
      >
       {label}
      </th>
     );
    })}
   </tr>
  </thead>
 );
};

export default TableHead;

At the moment, on clicking the table header, we pass along their unique accessor. As seen in the handler, we are only logging it.

Let’s save the file and open the console while clicking the table headers. We should see their respective accessor keys except the column with the sortable value of false.

Next, let’s define the logic to switch the order on every header click by updating the handleSortingChange handler so we have the following:

const handleSortingChange = (accessor) => {
 const sortOrder =
  accessor === sortField && order === "asc" ? "desc" : "asc";
 setSortField(accessor);
 setOrder(sortOrder);
 handleSorting(accessor, sortOrder);
};

At this point, we have access to the latest sort order. Now, to manipulate the table data, we must pass the order up a level to the parent Table component, and we’ve done that using the handleSorting() function call.

This is because the Table component is the one that holds the data in the state. And therefore, it is the only component that can change it. In React, we can raise an event like we did in this file and then handle it in the parent component via the props.

So, before we save the file, let’s ensure we destructure the component prop and have access to the handleSorting like so:

const TableHead = ({ columns, handleSorting }) => {

Now, save the file.

Next, let’s open the components/Table.js file to handle the event. First, in the return, let’s ensure we pass the handleSorting to the TableHead instance as a prop:

return (
 <>
  <table className="table">
   {/* ... */}
   <TableHead columns={columns} handleSorting={handleSorting} />
   <TableBody columns={columns} tableData={tableData} />
  </table>
 </>
);

We can rewrite the above to look simpler, like so:

return (
 <>
  <table className="table">
   {/* ... */}
   <TableHead {...{ columns, handleSorting }} />
   <TableBody {...{ columns, tableData }} />
  </table>
 </>
);

Either of the above methods is fine.

Finally, let’s add the handleSorting handler above the return statement:

const handleSorting = (sortField, sortOrder) => {
 console.log(sortField, sortOrder)
};

Let’s save all files.

The handleSorting handler expects two parameters because we passed them from the TableHead component. In the meantime, we log these parameters to the console whenever we click the table headers.

Next, we will use the sort() function alongside the localeCompare() to properly sort the table data. Fortunately, we learned that earlier in this tutorial.

By applying the sorting logic, the handleSorting handler now looks like this:

const handleSorting = (sortField, sortOrder) => {
 if (sortField) {
  const sorted = [...tableData].sort((a, b) => {
   return (
    a[sortField].toString().localeCompare(b[sortField].toString(), "en", {
     numeric: true,
    }) * (sortOrder === "asc" ? 1 : -1)
   );
  });
  setTableData(sorted);
 }
};

The code should be clear enough. If you need a refresher, please revisit the earlier explanation. Here, we sort the table data by the column headers and then update the tableData state via the setTableData() updater function.

Notice how we reverse the sort order by checking for the "asc" value and switch the returned value.

Let’s save and test our projects.

The project should work until we click on the column that includes null values. Let’s handle that by updating the handler to check for null values:

const handleSorting = (sortField, sortOrder) => {
 if (sortField) {
  const sorted = [...tableData].sort((a, b) => {
   if (a[sortField] === null) return 1;
   if (b[sortField] === null) return -1;
   if (a[sortField] === null && b[sortField] === null) return 0;
   return (
    a[sortField].toString().localeCompare(b[sortField].toString(), "en", {
     numeric: true,
    }) * (sortOrder === "asc" ? 1 : -1)
   );
  });
  setTableData(sorted);
 }
};

Now, save the file and test the project. It should work.

Displaying icons to indicate the sorting direction

This is straightforward. Here, let’s dynamically add the default, up, and down class names to the table header, th, element. These classes are already styled in our CSS file to add arrow icons. In the components/TableHead.js file, update the return statement so we have the following:

return (
 <thead>
  <tr>
   {columns.map(({ label, accessor, sortable }) => {
    const cl = sortable
    ? sortField && sortField === accessor && order === "asc"
     ? "up"
     : sortField && sortField === accessor && order === "desc"
     ? "down"
     : "default"
    : "";
    return (
     <th
      key={accessor}
      onClick={sortable ? () => handleSortingChange(accessor) : null}
      className={cl}
     >
      {label}
     </th>
    );
   })}
  </tr>
 </thead>
);

In the code, we use the nested ternary operator to check the order status and assign class names accordingly. Save and test your project.

That’s pretty much it!

Conclusion

Adding sorting functionality to a table is vital for data management and improves user experience, especially for a table of many rows. In this tutorial, we learned how to add the sort feature to a React table without using any library.

If you liked this tutorial, endeavor to share it around the web. And, if you have questions or contributions, please share your thoughts in the comment section.

You can find the project source code on my GitHub.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Ibadehin Mojeed I'm an advocate of project-based learning. I also write technical content around web development.

Leave a Reply