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
- Core Table Structure
- Data Management
- Sorting
- Filtering
- Pagination
- Column Resizing
- Column Reordering
- Row Selection
- Expandable Rows
- Fixed Headers and Columns
- Virtual Scrolling
- Customizable Cell Rendering
- Theming and Styling
- Accessibility
- Performance Optimization
- Export Functionality
- Integration with State Management
- 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😁).
Top comments (0)