DEV Community

Hamidul Islam
Hamidul Islam

Posted on

Ultimate React Reusable Table Component

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;
Enter fullscreen mode Exit fullscreen mode

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;
  });
};
Enter fullscreen mode Exit fullscreen mode

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',
  }));
};
Enter fullscreen mode Exit fullscreen mode

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 }));
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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' }}>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
  }
`;
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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.
});
Enter fullscreen mode Exit fullscreen mode

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😁).

Top comments (0)