Skip to content

[Refactoring] Components: Break down 800+ line React components using composition patterns #330

@syed-reza98

Description

@syed-reza98

Problem

Large React component files (>800 lines) mix multiple concerns in a single file, violating separation of concerns and making code harder to maintain:

  1. src/app/store/[slug]/checkout/page.tsx (836 lines)

    • Form validation schema, UI rendering, API calls, state management, payment method handling, cart integration
  2. src/components/orders-table.tsx (977 lines)

    • Table columns definition, SSE/polling logic, API fetching, filtering, sorting, pagination, real-time updates
  3. src/components/product-edit-form.tsx (970 lines)

    • Form validation, variant management, image upload, API calls, UI rendering, discount handling
  4. src/components/product-form.tsx (940 lines)

    • Nearly identical structure to product-edit-form with duplicated logic

These large components suffer from:

  • Hard to understand: Developers must read 800+ lines to understand component behavior
  • Difficult to test: Testing requires mocking many dependencies and covering many scenarios
  • Poor reusability: Logic is tightly coupled to specific components
  • Slow rendering: Large component trees with complex state management
  • High cognitive load: Multiple responsibilities increase mental overhead

Current Code Location

  • Files:
    • src/app/store/[slug]/checkout/page.tsx (836 lines)
    • src/components/orders-table.tsx (977 lines)
    • src/components/product-edit-form.tsx (970 lines)
    • src/components/product-form.tsx (940 lines)
  • Complexity: Very High - Multiple concerns per component

Proposed Refactoring

Decompose large components using composition patterns:

Benefits

  • Single Responsibility: Each component/hook has one clear purpose
  • Easier testing: Small, focused unit tests
  • Better reusability: Extracted hooks and utilities can be reused
  • Improved performance: Smaller components re-render less frequently
  • Clearer structure: Component hierarchy documents intent
  • Parallel development: Teams can work on sub-components independently

Suggested Approach: CheckoutPage (836 lines)

Phase 1: Extract Custom Hooks

// src/hooks/useCheckoutForm.ts (~100 lines)
export function useCheckoutForm(storeId: string) {
  const form = useForm(CheckoutFormData)({ ... });
  const handleSubmit = async (data: CheckoutFormData) => { ... };
  return { form, handleSubmit, isSubmitting };
}

// src/hooks/usePaymentMethods.ts (~80 lines)
export function usePaymentMethods(storeApiUrl: string) {
  const [methods, setMethods] = useState([]);
  const [loading, setLoading] = useState(true);
  // Fetch and manage payment methods
  return { methods, loading, error };
}

// src/hooks/useDiscountCode.ts (~60 lines)
export function useDiscountCode(storeApiUrl: string) {
  const [discount, setDiscount] = useState(null);
  const applyCode = async (code: string) => { ... };
  const removeCode = () => { ... };
  return { discount, applyCode, removeCode, isApplying };
}

Phase 2: Extract Sub-Components

// src/components/checkout/customer-info-section.tsx (~80 lines)
export function CustomerInfoSection({ form }) {
  return (
    (Card)
      (CardHeader)Customer Information(/CardHeader)
      (CardContent)
        {/* Email, Name, Phone fields */}
      (/CardContent)
    (/Card)
  );
}

// src/components/checkout/shipping-address-section.tsx (~100 lines)
export function ShippingAddressSection({ form }) { ... }

// src/components/checkout/billing-address-section.tsx (~100 lines)
export function BillingAddressSection({ form, showBilling }) { ... }

// src/components/checkout/payment-method-section.tsx (~120 lines)
export function PaymentMethodSection({ form, methods }) { ... }

// src/components/checkout/order-summary-section.tsx (~150 lines)
export function OrderSummarySidePanel({ cart, discount, onApplyCode }) { ... }

Phase 3: Refactor Main Component

// src/app/store/[slug]/checkout/page.tsx (~200 lines)
export default function CheckoutPage() {
  const { form, handleSubmit, isSubmitting } = useCheckoutForm(storeId);
  const { methods, loading: methodsLoading } = usePaymentMethods(storeApiUrl);
  const { discount, applyCode, removeCode } = useDiscountCode(storeApiUrl);
  const { cart } = useCart();

  return (
    (div className="checkout-layout")
      (Form {...form})
        (form onSubmit={form.handleSubmit(handleSubmit)})
          (CustomerInfoSection form={form} /)
          (ShippingAddressSection form={form} /)
          (BillingAddressSection 
            form={form} 
            showBilling={!form.watch('billingSameAsShipping')} 
          /)
          (PaymentMethodSection form={form} methods={methods} /)
        (/form)
      (/Form)
      
      (OrderSummarySidePanel
        cart={cart}
        discount={discount}
        onApplyCode={applyCode}
        onRemoveCode={removeCode}
      /)
    (/div)
  );
}

Suggested Approach: OrdersTable (977 lines)

Extract Column Definitions:

// src/components/orders/columns.tsx (~150 lines)
export function useOrdersTableColumns(): ColumnDef(Order)[] {
  return [
    { id: 'orderNumber', ... },
    { id: 'customer', ... },
    { id: 'status', ... },
    // ... other columns
  ];
}

Extract Real-time Logic:

// src/hooks/useOrdersRealtime.ts (~120 lines)
export function useOrdersRealtime(storeId: string, mode: UpdateMode) {
  const [orders, setOrders] = useState([]);
  const { streamedOrders } = useOrderStream(storeId, mode === 'sse');
  // Handle SSE vs polling logic
  return { orders, refresh, connectionStatus };
}

Simplified Component:

// src/components/orders-table.tsx (~350 lines)
export function OrdersTable({ storeId }) {
  const columns = useOrdersTableColumns();
  const { orders, refresh, status } = useOrdersRealtime(storeId, updateMode);
  const table = useReactTable({ data: orders, columns, ... });
  
  return (
    (Card)
      (OrdersTableHeader status={status} onRefresh={refresh} /)
      (OrdersTableContent table={table} /)
      (OrdersTablePagination table={table} /)
    (/Card)
  );
}

Code Example

Before (CheckoutPage - 836 lines):

export default function CheckoutPage() {
  // 100+ lines of state declarations
  const [paymentMethods, setPaymentMethods] = useState([]);
  const [loading, setLoading] = useState(true);
  const [discount, setDiscount] = useState(null);
  // ... many more states
  
  // 200+ lines of useEffect hooks
  useEffect(() => { /* fetch payment methods */ }, []);
  useEffect(() => { /* validate cart */ }, []);
  useEffect(() => { /* calculate totals */ }, []);
  // ... many more effects
  
  // 300+ lines of JSX with deeply nested structure
  return (
    (div)
      {/* 300+ lines of form fields, cards, inputs */}
    (/div)
  );
}

After (CheckoutPage - ~200 lines):

export default function CheckoutPage() {
  const { form, handleSubmit } = useCheckoutForm(storeId);
  const { methods } = usePaymentMethods(storeApiUrl);
  const { discount, applyCode } = useDiscountCode(storeApiUrl);

  return (
    (CheckoutLayout)
      (Form {...form})
        (CustomerInfoSection form={form} /)
        (ShippingAddressSection form={form} /)
        (PaymentMethodSection form={form} methods={methods} /)
      (/Form)
      (OrderSummarySidePanel discount={discount} onApplyCode={applyCode} /)
    (/CheckoutLayout)
  );
}

Impact Assessment

  • Effort: Medium-High per component (~1-2 days each)
  • Risk: Low-Medium - UI components are well-isolated, comprehensive tests mitigate risk
  • Benefit: Very High - Dramatically improves code readability and maintainability
  • Priority: High - Large components are a common source of bugs and slow development

Refactoring Sequence

Priority 1: CheckoutPage (836 lines)

  • High user impact, complex business logic
  • Extract hooks and sub-components
  • Est: 2 days

Priority 2: OrdersTable (977 lines)

  • Real-time complexity, many features
  • Extract columns, filters, real-time logic
  • Est: 2 days

Priority 3: Product Forms (940 + 970 = 1,910 lines combined)

  • Extract shared logic between create/edit
  • Consider unified ProductFormBase component
  • Est: 3 days

Related Files

Changes per component:

  • Main component file (decomposed)
  • New hook files (src/hooks/use*.ts)
  • New sub-component files (src/components/[feature]/*.tsx)
  • Tests for extracted hooks and components

Testing Strategy

  1. Snapshot tests for existing components (baseline)
  2. Extract one hook/component at a time with unit tests
  3. Integration tests ensure sub-components work together
  4. Visual regression tests (Playwright) verify no UI changes
  5. Manual E2E testing: Complete user flows (checkout, viewing orders, etc.)

Additional Context

Pattern to follow (from Vercel's React best practices):

  • Keep components under 250 lines
  • Extract business logic into custom hooks
  • Use composition over inheritance
  • Colocate related components in feature folders

Other large components to consider in future:

  • src/components/integrations/facebook/dashboard.tsx (1,106 lines)
  • src/components/order-detail-client.tsx (857 lines)
  • src/components/data-table.tsx (811 lines)

Generated by Daily Codebase Analyzer - Semantic Function Extraction & Refactoring

  • expires on Mar 22, 2026, 9:09 PM UTC

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions