Custom React Table With Filter and Sorting - No Libraries
In this post you will learn how to create a custom table inside React with filtering and sorting.
And actually the first thought that you for sure have "I must use some library. For example react-table if I want to implement tables inside React". Maybe you need a library if you have lots of different tables and you don't want to write anything.
But actually it is not that difficult to create a table on your own and if you just need one table it doesn't make a lot of sense to use a library.
This is why in this post we will implement a users table which will render our users not just like static data but with fetching them from API. Additionally we will implement there filtering and sorting.
Fetching data
I already prepared an empty React project. All our logic regarding the table we will pack in a single file as it is not that big, but we can split it later.
// src/usersTable/UsersTable.jsx
const UsersTable = () => {
const [users, setUsers] = useState([]);
console.log(users)
useEffect(() => {
const url = `http://localhost:3004/users`;
fetch(url)
.then((res) => res.json())
.then((users) => {
setUsers(users);
});
}, []);
}
export default UsersTable;
Here we defined our UsersTable
component. We created a state for the list of users and added useEffect
with fetch on initialize of our application.
You might wonder what this url returns? I used json-server package to implement a mocked response from the API. For this you just need to create a db.json file in your root folder with such content.
{
"users": [
{
"id": 1,
"name": "Jack",
"age": 20
},
{
"id": 2,
"name": "John",
"age": 25
},
{
"id": 3,
"name": "Mike",
"age": 30
}
]
}
Now we can start our API server with
npm i -g json-server
json-server --watch db.json
As you can see in browser we loaded data in our component and they are available for us in our state.
Rendering markup
Now we must render our table.
// src/usersTable/UsersTable.jsx
const UsersTable = () => {
return (
<div>
SEARCH BAR
<table>
<Header/>
</table>
</div>
)
}
Here is our basic table markup. In order to create Header
component we must define columns that we will render.
const UsersTable = () => {
const columns = ["id", "name", "age"];
}
Now I want to create a Header
component. We could move it to other file but as it is not big I will leave it here.
// src/usersTable/UsersTable.jsx
const Header = ({ columns }) => {
return (
<thead>
<tr>
{columns.map((column) => (
<th key={column}>{column}</th>
))}
</tr>
</thead>
);
};
In order to render our header we used columns
property. In this way to don't duplication code for every column.
As you can see our basic header is rendered but styling doesn't look nice. This is why let's create usersTable.css
file.
// src/usersTable/UsersTable.css
.users-table {
table-layout: fixed;
width: 100%;
border-collapse: collapse;
}
.users-table-cell {
border: 1px solid #dddddd;
text-align: left;
padding: 8px;
}
And apply these styles to our components.
const Header = ({ columns }) => {
return (
<thead>
<tr>
{columns.map((column) => (
<th key={column} className='users-table-cell'>{column}</th>
))}
</tr>
</thead>
);
};
const UsersTable = () => {
return (
<div>
SEARCH BAR
<table className="users-table">
<Header/>
</table>
</div>
)
}
As you can see now it looks much better and our styling is applied.
Now it is time to render our content. Let's create a new component for this
const Content = ({ entries, columns }) => {
return (
<tbody>
{entries.map((entry) => (
<tr key={entry.id}>
{columns.map((column) => (
<td key={column} className="users-table-cell">
{entry[column]}
</td>
))}
</tr>
))}
</tbody>
);
};
<Content entries={users} columns={columns} />
To render our content we created Content
component. To avoid code duplication we used entries
together with columns
to map all cells.
Now in browser we can see that our content is rendered and our users are in the list. Additionally our component is fully asynchronous and we get our data from the API.
Adding sorting
But we can make our table even better, we can implement sorting. Here we must change our Header
component as we need to implement logic regarding sorting. But the first step to start is to create a new state.
const UsersTable = () => {
...
const [sorting, setSorting] = useState({ column: "id", order: "asc" });
}
Here we defined sorting as an object and set default column and it's sorting order.
As we always have sorting our code is easier to support because we don't need to check for no sorting
Let's move a header cell code to additional component because we need a bit of logic there.
const Header = ({ columns, sorting }) => {
return (
<thead>
<tr>
{columns.map((column) => (
<HeaderCell
column={column}
sorting={sorting}
key={column}
/>
))}
</tr>
</thead>
);
};
Here instead of th
we rendered HeaderCell
and provided inside sorting
to know what to show.
const HeaderCell = ({ column, sorting }) => {
const isDescSorting = sorting.column === column && sorting.order === "desc";
const isAscSorting = sorting.column === column && sorting.order === "asc";
return (
<th
key={column}
className="users-table-cell"
>
{column}
{isDescSorting && <span>▼</span>}
{isAscSorting && <span>▲</span>}
</th>
);
};
Our HeaderCell
is just a th
tag but also icons for sorting. We render arrow down or up icon if this specific column is sorted and the order matches.
As you can see our icon of sorting is rendered in the ID column. Now we need to resort our data correctly.
const HeaderCell = ({ column, sorting, sortTable }) => {
const isDescSorting = sorting.column === column && sorting.order === "desc";
const isAscSorting = sorting.column === column && sorting.order === "asc";
const futureSortingOrder = isDescSorting ? "asc" : "desc";
return (
<th
key={column}
className="users-table-cell"
onClick={() => sortTable({ column, order: futureSortingOrder })}
>
{column}
{isDescSorting && <span>▼</span>}
{isAscSorting && <span>▲</span>}
</th>
);
};
Here we defined a futureSortingOrder
which is the sorting that we want to set. We also added onClick
event which calls sortTable
function where we provide our sorting object.
const UsersTable = () => {
...
const sortTable = (newSorting) => {
setSorting(newSorting);
};
useEffect(() => {
const url = `http://localhost:3004/users?_sort=${sorting.column}&_order=${sorting.order}`;
fetch(url)
.then((res) => res.json())
.then((users) => {
setUsers(users);
});
}, [sorting]);
return (
<div>
SEARCH BAR
<table className="users-table">
<Header columns={columns} sorting={sorting} sortTable={sortTable} />
<Content entries={users} columns={columns} />
</table>
</div>
);
};
In our UsersTable
we defined a sortTable
function which we pass to our Header
component. This function simply updates our sorting state.
Additionally we set sorting
as a dependency to useEffect
to it is retriggered every time when we change sorting. We also provided _sorting
and _order
to our API url to get updated data.
As you can see in browser our data is resorted after clicking on the header cell.
Filtering
Our sorting functions correctly and now we are missing only filter functionality. So what we want to do is build a from with the input. When we hit enter our data must by filtered by making new API request.
const SearchBar = ({ searchTable }) => {
const [searchValue, setSearchValue] = useState("");
const submitForm = (e) => {
e.preventDefault();
searchTable(searchValue);
};
return (
<div className="search-bar">
<form onSubmit={submitForm}>
<input
type="text"
placeholder="Search..."
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
/>
</form>
</div>
);
};
Here we created new SearchBar
component. It has inner start searchValue
when we type in our input. When we submit a form we call searchTable
function which is a prop of our component.
Now let's implement search logic in our main component.
const UsersTable = () => {
const [searchValue, setSearchValue] = useState("");
...
const searchTable = (newSearchValue) => {
setSearchValue(newSearchValue);
};
useEffect(() => {
const url = `http://localhost:3004/users?_sort=${sorting.column}&_order=${sorting.order}&name_like=${searchValue}`;
fetch(url)
.then((res) => res.json())
.then((users) => {
setUsers(users);
});
}, [sorting, searchValue]);
return (
<div>
<SearchBar searchTable={searchTable} />
<table className="users-table">
<Header columns={columns} sorting={sorting} sortTable={sortTable} />
<Content entries={users} columns={columns} />
</table>
</div>
);
};
Here we created searchValue
as a state in our UsersTable
. When we call searchTable
function is simply changes this state. Additionally we put searchValue
as a parameter to our API url and provided searchValue
as a dependency of our useEffect
. It means that every time when we set searchValue
our API will be refetched.
And last but not least we rendered SearchBar
component in our markup.
As you can see in browser our filtering functionality works just fine.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!
📚 Source code of what we've done