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.
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": "wg@creative.org", "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.
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.
The post Creating a React sortable table appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/gqcosR7
via Read more