Breaking Up with Our Monolithic Table: A React Refactoring Journey

Refactoring a React Table Component: From Monolith to Compound Pattern The Challenge We had a complex business table component that started simple but grew unwieldy over time. Built on top of a basic table, it handled API integration, filtering, sorting, row selection, custom rendering, and more - all in a single monolithic component. // Before: A monolithic approach with numerous props and internal state This approach led to several problems: TypeScript errors: Implicit any types and improper generics Prop drilling nightmare: Dozens of props passed through multiple layers Limited customization: Hard to customize just part of the table Maintenance headaches: Changes in one feature affected others The Solution: Compound Component Pattern After analyzing the issues, we decided to refactor using the compound component pattern: // After: A composable approach with specialized components sharing context Key Implementation Steps Create a Context System: We built a context to share state between components // Simplified TableContext export const TableContext = createContext({ state: { rows: [], columns: [], loading: false }, actions: { refresh: () => {}, toggleRowSelection: () => {} }, }); // Type-safe hook export const useTableContext = () => { return useContext(TableContext); }; Split the Monolith: We broke down the component into specialized parts BusinessTable/ ├── components/ // Specialized components ├── contexts/ // Context system ├── hooks/ // Custom hooks ├── utils/ // Helper functions └── BusinessTable.tsx // Main entry point Ensure Backward Compatibility: We maintained the original API while adding the new pattern // Original monolithic API still works export const BusinessTable = (props) => { // Internally uses the compound components return ( {/* Default structure */} ); }; // Attach compound components Object.assign(BusinessTable, BusinessTableCompound); Fix TypeScript Issues: We solved circular dependencies and improved type safety // Barrel pattern to avoid circular imports export * from './TableProvider'; export * from './TableStructure'; // Generic types for API integration export interface TableColumn extends BaseColumn { // Type-safe column definition } Benefits We've Gained Better Developer Experience Clear component hierarchy and improved TypeScript support Specialized hooks for API integration Enhanced Customization Replace only the parts you need Insert custom components anywhere in the structure Style individual components without affecting others Improved Maintainability Isolated changes don't affect the entire table Easier testing of individual components Better separation of concerns Type Safety Proper generic types for API integration Eliminated implicit any types Better IDE support with accurate type hints Key Takeaways for Your Next Refactoring Context is powerful for sharing state between components without prop drilling Compound components excel for complex UI elements with many configuration options Backward compatibility is crucial - don't break existing code TypeScript generics require careful planning but provide excellent type safety Barrel pattern helps avoid circular dependencies in a component library This refactoring pattern can be applied to many complex React components beyond tables - modals, forms, or any component that has grown too large and difficult to maintain. Have you refactored complex React components? What patterns have you found helpful? Share your experiences in the comments!

May 19, 2025 - 01:00
 0
Breaking Up with Our Monolithic Table: A React Refactoring Journey

Refactoring a React Table Component: From Monolith to Compound Pattern

The Challenge

We had a complex business table component that started simple but grew unwieldy over time. Built on top of a basic table, it handled API integration, filtering, sorting, row selection, custom rendering, and more - all in a single monolithic component.

// Before: A monolithic approach with numerous props and internal state
<BusinessTable
  apiId="api-identifier"
  apiEndpoint="GET /resources"
  customRef={tableRef}
  apiParams={params}
  additionalActions={<>/* Action buttons */}}
  selectable="single"
  columns={resourceColumns}
  pageSize={10}
  additionalFilters={<>/* Custom filters */}}
  // ...many more props
/>

This approach led to several problems:

  • TypeScript errors: Implicit any types and improper generics
  • Prop drilling nightmare: Dozens of props passed through multiple layers
  • Limited customization: Hard to customize just part of the table
  • Maintenance headaches: Changes in one feature affected others

The Solution: Compound Component Pattern

After analyzing the issues, we decided to refactor using the compound component pattern:

// After: A composable approach with specialized components sharing context
<BusinessTable.Provider
  apiId="api-identifier"
  apiEndpoint="GET /resources"
  apiParams={params}
  columns={resourceColumns}
>
  <BusinessTable.Structure ref={tableRef}>
    <BusinessTable.Toolbar>
      <BusinessTable.RefreshButton />
      <BusinessTable.Filter />
      <BusinessTable.Settings />
    BusinessTable.Toolbar>
    <BusinessTable.Header />
    <BusinessTable.Body />
    <BusinessTable.Footer>
      <BusinessTable.Pagination />
    BusinessTable.Footer>
  BusinessTable.Structure>
BusinessTable.Provider>

Key Implementation Steps

  1. Create a Context System: We built a context to share state between components
// Simplified TableContext
export const TableContext = createContext({
  state: { rows: [], columns: [], loading: false },
  actions: { refresh: () => {}, toggleRowSelection: () => {} },
});

// Type-safe hook
export const useTableContext = <T, K, R>() => {
  return useContext(TableContext);
};
  1. Split the Monolith: We broke down the component into specialized parts
BusinessTable/
├── components/            // Specialized components
├── contexts/              // Context system
├── hooks/                 // Custom hooks
├── utils/                 // Helper functions
└── BusinessTable.tsx      // Main entry point
  1. Ensure Backward Compatibility: We maintained the original API while adding the new pattern
// Original monolithic API still works
export const BusinessTable = (props) => {
  // Internally uses the compound components
  return (
    <TableProvider {...props}>
      <TableStructure {...props}>{/* Default structure */}TableStructure>
    TableProvider>
  );
};

// Attach compound components
Object.assign(BusinessTable, BusinessTableCompound);
  1. Fix TypeScript Issues: We solved circular dependencies and improved type safety
// Barrel pattern to avoid circular imports
export * from './TableProvider';
export * from './TableStructure';

// Generic types for API integration
export interface TableColumn<T, K, R> extends BaseColumn {
  // Type-safe column definition
}

Benefits We've Gained

  1. Better Developer Experience
  • Clear component hierarchy and improved TypeScript support
  • Specialized hooks for API integration
  1. Enhanced Customization
  • Replace only the parts you need
  • Insert custom components anywhere in the structure
  • Style individual components without affecting others
  1. Improved Maintainability
  • Isolated changes don't affect the entire table
  • Easier testing of individual components
  • Better separation of concerns
  1. Type Safety
    • Proper generic types for API integration
    • Eliminated implicit any types
    • Better IDE support with accurate type hints

Key Takeaways for Your Next Refactoring

  1. Context is powerful for sharing state between components without prop drilling

  2. Compound components excel for complex UI elements with many configuration options

  3. Backward compatibility is crucial - don't break existing code

  4. TypeScript generics require careful planning but provide excellent type safety

  5. Barrel pattern helps avoid circular dependencies in a component library

This refactoring pattern can be applied to many complex React components beyond tables - modals, forms, or any component that has grown too large and difficult to maintain.

Have you refactored complex React components? What patterns have you found helpful? Share your experiences in the comments!