Building a Reusable Table Component with Hooks


Published on 11.10.2019

When building React applications, tables are a common component to display data. They may look simple, but tables often need features like sorting, filtering, and pagination to enhance the user experience, especially when dealing with large datasets.

In this post, we’ll walk through how to build a reusable table component in React using Hooks. By the end of this tutorial, you’ll have a flexible table component that you can drop into any part of your app with minimal setup.

Let’s get started!


Why Build a Reusable Table Component?

A reusable table component saves time and effort by abstracting common functionality such as:

  • Sorting: Allow users to sort data based on column values.
  • Filtering: Let users filter rows based on search criteria.
  • Pagination: Break up long datasets into smaller, paginated chunks for easier viewing.

This reusable table component will have these features, all while being configurable and flexible for various use cases.


Step 1: Setting Up the Project

Let’s start by creating a new React project if you don’t have one already:

npx create-react-app reusable-table
cd reusable-table
npm start

Once your app is set up, we can start by creating our Table component.


Step 2: Create the Table Component

First, let’s create a basic structure for the table. The table should accept the following props:

  • data: An array of objects that holds the table data.
  • columns: An array that defines the columns to display in the table.

Create a new file Table.js inside the src directory.

// src/Table.js
import React from 'react';

const Table = ({ columns, data }) => {
  return (
    <table>
      <thead>
        <tr>
          {columns.map((col) => (
            <th key={col.accessor}>{col.Header}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map((row, idx) => (
          <tr key={idx}>
            {columns.map((col) => (
              <td key={col.accessor}>{row[col.accessor]}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
};

export default Table;

This basic table component will display the data in a tabular format using the columns and data props. The columns prop defines the table headers, and the data prop defines the rows.


Step 3: Add Sorting Functionality

Now, let’s add the ability to sort the table data when users click on the column headers. We can manage sorting state using the useState hook.

Modify the Table.js file to add sorting functionality:

// src/Table.js
import React, { useState } from 'react';

const Table = ({ columns, data }) => {
  const [sortBy, setSortBy] = useState({ key: '', order: 'asc' });

  const handleSort = (column) => {
    const isAsc = sortBy.key === column.accessor && sortBy.order === 'asc';
    setSortBy({ key: column.accessor, order: isAsc ? 'desc' : 'asc' });
  };

  const sortedData = React.useMemo(() => {
    const { key, order } = sortBy;
    if (!key) return data;
    return [...data].sort((a, b) => {
      const aValue = a[key];
      const bValue = b[key];
      if (order === 'asc') {
        return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
      } else {
        return aValue < bValue ? 1 : aValue > bValue ? -1 : 0;
      }
    });
  }, [data, sortBy]);

  return (
    <table>
      <thead>
        <tr>
          {columns.map((col) => (
            <th key={col.accessor} onClick={() => handleSort(col)}>
              {col.Header}
              {sortBy.key === col.accessor ? (sortBy.order === 'asc' ? ' 🔼' : ' 🔽') : ''}
            </th>
          ))}
        </tr>
      </thead>
      <tbody>
        {sortedData.map((row, idx) => (
          <tr key={idx}>
            {columns.map((col) => (
              <td key={col.accessor}>{row[col.accessor]}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
};

export default Table;

Explanation:

  • We added useState to manage the current sorting state (sortBy), which keeps track of the column to sort by and the order (asc or desc).
  • We added a handleSort function that toggles the sorting order when a column header is clicked.
  • The sortedData variable holds the sorted data, which is computed using the useMemo hook to ensure that sorting only happens when necessary (i.e., when data or sortBy changes).
  • Each column header has an indicator (🔼 or 🔽) to show whether it’s sorted in ascending or descending order.

Step 4: Add Filtering Functionality

Next, we’ll implement a simple search filter that lets users search through the table’s rows based on a search term. We’ll use useState to manage the search term.

Here’s the modified Table.js with filtering:

// src/Table.js
import React, { useState } from 'react';

const Table = ({ columns, data }) => {
  const [sortBy, setSortBy] = useState({ key: '', order: 'asc' });
  const [filter, setFilter] = useState('');

  const handleSort = (column) => {
    const isAsc = sortBy.key === column.accessor && sortBy.order === 'asc';
    setSortBy({ key: column.accessor, order: isAsc ? 'desc' : 'asc' });
  };

  const handleFilterChange = (e) => {
    setFilter(e.target.value);
  };

  const filteredData = React.useMemo(() => {
    return data.filter((row) => {
      return columns.some((col) => {
        return row[col.accessor].toString().toLowerCase().includes(filter.toLowerCase());
      });
    });
  }, [data, filter, columns]);

  const sortedData = React.useMemo(() => {
    const { key, order } = sortBy;
    if (!key) return filteredData;
    return [...filteredData].sort((a, b) => {
      const aValue = a[key];
      const bValue = b[key];
      if (order === 'asc') {
        return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
      } else {
        return aValue < bValue ? 1 : aValue > bValue ? -1 : 0;
      }
    });
  }, [filteredData, sortBy]);

  return (
    <div>
      <input
        type="text"
        placeholder="Search..."
        value={filter}
        onChange={handleFilterChange}
      />
      <table>
        <thead>
          <tr>
            {columns.map((col) => (
              <th key={col.accessor} onClick={() => handleSort(col)}>
                {col.Header}
                {sortBy.key === col.accessor ? (sortBy.order === 'asc' ? ' 🔼' : ' 🔽') : ''}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {sortedData.map((row, idx) => (
            <tr key={idx}>
              {columns.map((col) => (
                <td key={col.accessor}>{row[col.accessor]}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default Table;

Explanation:

  • We added an input field for the search filter at the top of the table. The filter state manages the current search term.
  • The filteredData variable is computed using useMemo, which filters the data based on the search term entered by the user.
  • The sortedData is then derived from the filtered data, ensuring that both sorting and filtering are applied in sequence.

Step 5: Add Pagination

Finally, let’s add basic pagination to the table to display a limited number of rows per page. We’ll use useState to keep track of the current page and rows per page.

// src/Table.js
import React, { useState } from 'react';

const Table = ({ columns, data, rowsPerPage = 5 }) => {
  const [sortBy, setSortBy] = useState({ key: '', order: 'asc' });
  const [filter, setFilter] = useState('');
  const [currentPage, setCurrentPage] = useState(1);

  const handleSort = (column) => {
    const isAsc = sortBy.key === column.accessor && sortBy.order === 'asc';
    setSortBy({ key: column.accessor, order: isAsc ? 'desc' : 'asc' });
  };

  const handleFilterChange = (e) => {
    setFilter(e.target.value);
  };

  const handlePageChange = (page) => {
    setCurrentPage(page);
  };

  const filteredData = React.useMemo(() => {
    return data.filter((row) => {
      return columns.some((col) => {
        return row[col.accessor].toString().toLowerCase().includes(filter.toLowerCase());
      });
    });
  }, [data, filter, columns]);

  const sortedData = React.useMemo(() => {
    const { key, order } = sortBy;
    if (!key) return filteredData;
    return [...filteredData].sort((a, b) => {
      const aValue = a[key];
      const bValue = b[key];
      if (order === 'asc') {
        return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
      } else {
        return aValue < bValue ? 1 : aValue > bValue ? -1 : 0;
      }
    });
  }, [filteredData, sortBy]);

  const paginatedData = React.useMemo(() => {
    const startIdx = (currentPage - 1) * rowsPerPage;
    const endIdx = startIdx + rowsPerPage;
    return sortedData.slice(startIdx, endIdx);
  }, [sortedData, currentPage, rowsPerPage]);

  const totalPages = Math.ceil(sortedData.length / rowsPerPage);

  return (
    <div>
      <input
        type="text"
        placeholder="Search..."
        value={filter}
        onChange={handleFilterChange}
      />
      <table>
        <thead>
          <tr>
            {columns.map((col) => (
              <th key={col.accessor} onClick={() => handleSort(col)}>
                {col.Header}
                {sortBy.key === col.accessor ? (sortBy.order === 'asc' ? ' 🔼' : ' 🔽') : ''}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {paginatedData.map((row, idx) => (
            <tr key={idx}>
              {columns.map((col) => (
                <td key={col.accessor}>{row[col.accessor]}</td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
      <div>
        <button onClick={() => handlePageChange(currentPage - 1)} disabled={currentPage === 1}>
          Prev
        </button>
        <span>Page {currentPage} of {totalPages}</span>
        <button onClick={() => handlePageChange(currentPage + 1)} disabled={currentPage === totalPages}>
          Next
        </button>
      </div>
    </div>
  );
};

export default Table;

Explanation:

  • We added currentPage to manage the current page state, and rowsPerPage to control how many rows are displayed per page.
  • paginatedData is computed based on the current page and the rows to display per page.
  • The pagination buttons allow the user to navigate between pages.

Conclusion

In this post, we’ve built a reusable table component in React using Hooks. The table component supports:

  • Sorting by column.
  • Filtering data based on a search term.
  • Pagination to manage large datasets.

This component is flexible and reusable, allowing you to easily incorporate tables with sorting, filtering, and pagination in any React app.

By leveraging React’s Hooks, we’ve built a component that’s both modern and powerful, offering the benefits of functional components while keeping the logic clean and maintainable.

Happy coding!


Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *