Ultimate React Reusable Table Component



This content originally appeared on DEV Community and was authored by Hamidul Islam

In this comprehensive guide, we’ll walk through the process of building a highly flexible and feature-rich React table component that can handle various edge cases and provide a wide range of functionalities. This table component will be suitable for universal usage across different projects and scenarios.

Table of Contents

  1. Core Table Structure
  2. Data Management
  3. Sorting
  4. Filtering
  5. Pagination
  6. Column Resizing
  7. Column Reordering
  8. Row Selection
  9. Expandable Rows
  10. Fixed Headers and Columns
  11. Virtual Scrolling
  12. Customizable Cell Rendering
  13. Theming and Styling
  14. Accessibility
  15. Performance Optimization
  16. Export Functionality
  17. Integration with State Management
  18. Testing

1. Core Table Structure

Start by creating a basic table structure using semantic HTML elements:

import React from 'react';

const Table = ({ columns, data }) => {
  return (
    <table>
      <thead>
        <tr>
          {columns.map(column => (
            <th key={column.key}>{column.header}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map((row, rowIndex) => (
          <tr key={rowIndex}>
            {columns.map(column => (
              <td key={`${rowIndex}-${column.key}`}>{row[column.key]}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
};

export default Table;

2. Data Management

Implement a flexible data management system that can handle various data types and structures:

const processData = (data, columns) => {
  return data.map(row => {
    const processedRow = {};
    columns.forEach(column => {
      processedRow[column.key] = column.accessor ? column.accessor(row) : row[column.key];
    });
    return processedRow;
  });
};

3. Sorting

Add sorting functionality to allow users to sort data by clicking on column headers:

const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });

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

const handleSort = (key) => {
  setSortConfig(prevConfig => ({
    key,
    direction: prevConfig.key === key && prevConfig.direction === 'asc' ? 'desc' : 'asc',
  }));
};

4. Filtering

Implement a flexible filtering system that works with different data types:

const [filters, setFilters] = useState({});

const filteredData = React.useMemo(() => {
  return data.filter(row => {
    return Object.entries(filters).every(([key, value]) => {
      if (!value) return true;
      const cellValue = row[key]?.toString().toLowerCase();
      return cellValue?.includes(value.toLowerCase());
    });
  });
}, [data, filters]);

const handleFilterChange = (key, value) => {
  setFilters(prevFilters => ({ ...prevFilters, [key]: value }));
};

5. Pagination

Add pagination to handle large datasets efficiently:

const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);

const paginatedData = React.useMemo(() => {
  const startIndex = (currentPage - 1) * itemsPerPage;
  return filteredData.slice(startIndex, startIndex + itemsPerPage);
}, [filteredData, currentPage, itemsPerPage]);

const totalPages = Math.ceil(filteredData.length / itemsPerPage);

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

6. Column Resizing

Implement column resizing for better user control:

const [columnWidths, setColumnWidths] = useState({});

const handleColumnResize = (columnKey, newWidth) => {
  setColumnWidths(prevWidths => ({ ...prevWidths, [columnKey]: newWidth }));
};

// Apply column widths in your table cells
<td style={{ width: columnWidths[column.key] || 'auto' }}>

7. Column Reordering

Allow users to reorder columns using drag and drop:

const [columnOrder, setColumnOrder] = useState(columns.map(col => col.key));

const handleColumnReorder = (draggedColumnKey, targetColumnKey) => {
  const newOrder = [...columnOrder];
  const draggedIndex = newOrder.indexOf(draggedColumnKey);
  const targetIndex = newOrder.indexOf(targetColumnKey);
  newOrder.splice(draggedIndex, 1);
  newOrder.splice(targetIndex, 0, draggedColumnKey);
  setColumnOrder(newOrder);
};

// Use columnOrder to render columns in the desired order

8. Row Selection

Implement row selection functionality:

const [selectedRows, setSelectedRows] = useState(new Set());

const handleRowSelection = (rowId) => {
  setSelectedRows(prevSelected => {
    const newSelected = new Set(prevSelected);
    if (newSelected.has(rowId)) {
      newSelected.delete(rowId);
    } else {
      newSelected.add(rowId);
    }
    return newSelected;
  });
};

// Add checkboxes to your table rows for selection

9. Expandable Rows

Add support for expandable rows to show additional details:

const [expandedRows, setExpandedRows] = useState(new Set());

const toggleRowExpansion = (rowId) => {
  setExpandedRows(prevExpanded => {
    const newExpanded = new Set(prevExpanded);
    if (newExpanded.has(rowId)) {
      newExpanded.delete(rowId);
    } else {
      newExpanded.add(rowId);
    }
    return newExpanded;
  });
};

// Render expanded content when a row is expanded

10. Fixed Headers and Columns

Implement fixed headers and columns for better navigation of large datasets:

const TableWithFixedHeadersAndColumns = styled.div`
  .table-container {
    overflow: auto;
    height: 400px;
  }

  table {
    position: relative;
  }

  thead th {
    position: sticky;
    top: 0;
    background: white;
    z-index: 1;
  }

  .fixed-column {
    position: sticky;
    left: 0;
    background: white;
    z-index: 2;
  }
`;

11. Virtual Scrolling

Implement virtual scrolling for efficient rendering of large datasets:

import { FixedSizeList as List } from 'react-window';

const VirtualizedRows = ({ data, columns, rowHeight }) => {
  const Row = ({ index, style }) => (
    <div style={style}>
      {columns.map(column => (
        <div key={column.key}>{data[index][column.key]}</div>
      ))}
    </div>
  );

  return (
    <List
      height={400}
      itemCount={data.length}
      itemSize={rowHeight}
      width="100%"
    >
      {Row}
    </List>
  );
};

12. Customizable Cell Rendering

Allow custom cell rendering for flexibility:

const renderCell = (row, column) => {
  if (column.render) {
    return column.render(row[column.key], row);
  }
  return row[column.key];
};

// Use in your table cells
<td>{renderCell(row, column)}</td>

13. Theming and Styling

Implement a theming system for easy customization:

import { ThemeProvider, createGlobalStyle } from 'styled-components';

const theme = {
  colors: {
    primary: '#007bff',
    secondary: '#6c757d',
    // ... other color definitions
  },
  fonts: {
    body: 'Arial, sans-serif',
    // ... other font definitions
  },
  // ... other theme properties
};

const GlobalStyle = createGlobalStyle`
  // Define global styles using theme
`;

// Wrap your table component with ThemeProvider
<ThemeProvider theme={theme}>
  <GlobalStyle />
  <Table {...props} />
</ThemeProvider>

14. Accessibility

Ensure your table is accessible:

<table role="grid" aria-labelledby="tableTitle">
  <thead>
    <tr role="row">
      {columns.map(column => (
        <th role="columnheader" scope="col" key={column.key}>
          {column.header}
        </th>
      ))}
    </tr>
  </thead>
  <tbody>
    {data.map((row, rowIndex) => (
      <tr role="row" key={rowIndex}>
        {columns.map(column => (
          <td role="gridcell" key={`${rowIndex}-${column.key}`}>
            {row[column.key]}
          </td>
        ))}
      </tr>
    ))}
  </tbody>
</table>

15. Performance Optimization

Optimize performance using React.memo and useMemo:

const MemoizedRow = React.memo(({ row, columns, renderCell }) => (
  <tr>
    {columns.map(column => (
      <td key={column.key}>{renderCell(row, column)}</td>
    ))}
  </tr>
));

const Table = ({ data, columns }) => {
  const memoizedData = React.useMemo(() => processData(data, columns), [data, columns]);
  // ... other memoized values and rendering logic
};

16. Export Functionality

Add export functionality for CSV and Excel:

import { utils, writeFile } from 'xlsx';

const exportToExcel = (data, filename) => {
  const ws = utils.json_to_sheet(data);
  const wb = utils.book_new();
  utils.book_append_sheet(wb, ws, 'Sheet1');
  writeFile(wb, `${filename}.xlsx`);
};

const exportToCSV = (data, filename) => {
  const csvContent = data.map(row => Object.values(row).join(',')).join('\n');
  const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
  const link = document.createElement('a');
  if (link.download !== undefined) {
    const url = URL.createObjectURL(blob);
    link.setAttribute('href', url);
    link.setAttribute('download', `${filename}.csv`);
    link.style.visibility = 'hidden';
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  }
};

17. Integration with State Management

Integrate with popular state management solutions:

import { useSelector, useDispatch } from 'react-redux';
import { fetchData, sortData, filterData } from './tableSlice';

const TableContainer = () => {
  const dispatch = useDispatch();
  const { data, loading, error } = useSelector(state => state.table);

  useEffect(() => {
    dispatch(fetchData());
  }, [dispatch]);

  const handleSort = (key) => {
    dispatch(sortData(key));
  };

  const handleFilter = (filters) => {
    dispatch(filterData(filters));
  };

  // ... render Table component with props
};

18. Testing

Implement comprehensive testing:

import { render, screen, fireEvent } from '@testing-library/react';
import Table from './Table';

describe('Table Component', () => {
  const mockData = [
    { id: 1, name: 'John Doe', age: 30 },
    { id: 2, name: 'Jane Smith', age: 25 },
  ];

  const mockColumns = [
    { key: 'name', header: 'Name' },
    { key: 'age', header: 'Age' },
  ];

  it('renders table with correct data', () => {
    render(<Table data={mockData} columns={mockColumns} />);
    expect(screen.getByText('John Doe')).toBeInTheDocument();
    expect(screen.getByText('Jane Smith')).toBeInTheDocument();
  });

  it('sorts data when header is clicked', () => {
    render(<Table data={mockData} columns={mockColumns} />);
    fireEvent.click(screen.getByText('Name'));
    // Add assertions for sorted data
  });

  // Add more tests for filtering, pagination, etc.
});

19. The Fun(ctional) Conclusion

Congratulations! You’ve just completed a journey through the intricate world of React table components. If you’ve made it this far, you’re now equipped with the knowledge to create a table so powerful, it might just become sentient and start sorting itself.
Remember, with great table comes great responsibility. Your new table component is like a Swiss Army knife – it can do everything except make your coffee (I’m working on that feature for the next release😁).


This content originally appeared on DEV Community and was authored by Hamidul Islam