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
- 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);
};
- 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 (
<TableProvider {...props}>
<TableStructure {...props}>{/* Default structure */}</TableStructure>
</TableProvider>
);
};
// 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<T, K, R> 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!
Top comments (0)