Custom React Table With Filter and Sorting - No Libraries
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.

Finished project

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

Fetched array

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.

Basic header

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>
  )
}

Basic styling

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.

Rendered content

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.

Basic sorting

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.

Finished project

And actually if you are interested to learn how to build real React project from start to the end make sure to check my React hooks course.

📚 Source code of what we've done