DEV Community

Cover image for Breaking Up with Our Monolithic Table: A React Refactoring Journey
Mahmoud Abdulazim
Mahmoud Abdulazim

Posted on

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

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

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

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!

Top comments (0)