Skip to content

[Refactoring] Component: Decompose CheckoutPage into smaller, focused components #273

@syed-reza98

Description

@syed-reza98

Problem

The checkout page component (src/app/store/[slug]/checkout/page.tsx) is 829 lines and handles multiple concerns in a single component:

  • Cart validation and management (items, pricing, courier calculations)
  • Multi-step form logic (shipping, billing, payment selection)
  • Payment method selection UI and availability checking
  • Discount code validation and application
  • Order creation and submission
  • SSLCommerz payment initialization
  • 6+ useState hooks and complex state management
  • Multiple useEffect hooks for cart validation and sync

This creates several issues:

  • Hard to test - Component has too many responsibilities
  • Hard to maintain - Changes to one part risk breaking others
  • Hard to reuse - Logic is tightly coupled to this specific page
  • Poor performance - Large component re-renders more than necessary
  • Difficult to understand - New developers face a steep learning curve

Current Code Location

  • File: src/app/store/[slug]/checkout/page.tsx
  • Lines: 829 (very large for a single component)
  • Component: CheckoutPage
  • Complexity: High (multiple concerns, 6+ hooks, complex state)

Proposed Refactoring

Break down the monolithic checkout page into smaller, focused components and custom hooks:

Components to Extract:

  1. CheckoutCartSummary - Cart items display, pricing breakdown, courier charges
  2. CheckoutShippingForm - Shipping address form fields
  3. CheckoutBillingForm - Billing address form (conditional rendering)
  4. CheckoutPaymentMethod - Payment method selection cards
  5. CheckoutDiscountCode - Discount code input and validation

Hooks to Extract:

  1. useCheckoutCart - Cart validation, item removal, price calculations
  2. useDiscountCode - Discount validation and application logic
  3. useCheckoutSubmit - Order creation and payment initialization

Benefits

  • Improved testability - Each component/hook can be tested in isolation
  • Better reusability - Components can be used in other checkout-related flows
  • Clearer separation of concerns - Each component has a single responsibility
  • Easier maintenance - Changes are localized to specific components
  • Better performance - Smaller components re-render only when their props change
  • Improved developer experience - Easier to understand and modify
  • Type safety - Smaller components have simpler, more explicit prop types

Suggested Approach

Phase 1: Extract Display Components (Low Risk)

  1. Create CheckoutCartSummary.tsx for cart display
  2. Create CheckoutPaymentMethod.tsx for payment selection UI
  3. Update main checkout page to use new components

Phase 2: Extract Form Components (Medium Risk)
4. Create CheckoutShippingForm.tsx with form fields
5. Create CheckoutBillingForm.tsx with conditional billing fields
6. Pass form control from parent via props

Phase 3: Extract Custom Hooks (Higher Risk)
7. Create hooks/useCheckoutCart.ts for cart logic
8. Create hooks/useDiscountCode.ts for discount logic
9. Create hooks/useCheckoutSubmit.ts for submission logic
10. Refactor main page to use hooks

Phase 4: Testing & Optimization
11. Add unit tests for each component
12. Add unit tests for each hook
13. Add integration test for complete checkout flow
14. Optimize re-render behavior with React.memo if needed

Code Example

Before (current monolithic structure - simplified):

// src/app/store/[slug]/checkout/page.tsx (829 lines)
export default function CheckoutPage() {
  // 6+ useState hooks
  const [isProcessing, setIsProcessing] = useState(false);
  const [isValidating, setIsValidating] = useState(true);
  const [subtotal, setSubtotal] = useState(0);
  const [courierTotal, setCourierTotal] = useState(0);
  const [discount, setDiscount] = useState(0);
  const [discountCode, setDiscountCode] = useState("");
  
  // Cart state
  const items = useCart((state) => state.items);
  const removeItem = useCart((state) => state.removeItem);
  const getSubtotal = useCart((state) => state.getSubtotal);
  
  // Complex validation useEffect
  useEffect(() => {
    const validateCart = async () => {
      // 50+ lines of validation logic
    };
    validateCart();
  }, [items, storeSlug]);
  
  // Form handling
  const { register, handleSubmit, formState: { errors }, watch } = useForm({
    resolver: zodResolver(checkoutSchema),
  });
  
  // Payment method selection
  const paymentMethod = watch("paymentMethod");
  
  // Order submission (100+ lines)
  const onSubmit = async (data: CheckoutFormData) => {
    // Complex order creation logic
    // Payment initialization
    // Error handling
    // Redirect logic
  };
  
  return (
    (div)
      {/* 400+ lines of JSX mixing all concerns */}
      {/* Cart summary */}
      {/* Shipping form */}
      {/* Billing form */}
      {/* Payment method selection */}
      {/* Discount code input */}
      {/* Order summary */}
      {/* Submit button */}
    (/div)
  );
}

After (decomposed structure):

// src/app/store/[slug]/checkout/page.tsx (reduced to ~200 lines)
import { CheckoutCartSummary } from "@/components/checkout/checkout-cart-summary";
import { CheckoutShippingForm } from "@/components/checkout/checkout-shipping-form";
import { CheckoutBillingForm } from "@/components/checkout/checkout-billing-form";
import { CheckoutPaymentMethod } from "@/components/checkout/checkout-payment-method";
import { CheckoutDiscountCode } from "@/components/checkout/checkout-discount-code";
import { useCheckoutCart } from "@/hooks/useCheckoutCart";
import { useDiscountCode } from "@/hooks/useDiscountCode";
import { useCheckoutSubmit } from "@/hooks/useCheckoutSubmit";

export default function CheckoutPage() {
  const params = useParams<{ slug: string }>();
  const { storeUrl, storeApiUrl } = useStoreUrl();
  
  // Custom hooks handle complex logic
  const {
    items,
    isValidating,
    subtotal,
    courierTotal,
    handleRemoveItem,
  } = useCheckoutCart(params.slug, storeApiUrl);
  
  const {
    discount,
    discountCode,
    isValidatingDiscount,
    applyDiscountCode,
  } = useDiscountCode(storeApiUrl);
  
  const {
    isProcessing,
    submitOrder,
  } = useCheckoutSubmit(params.slug, storeApiUrl);
  
  // Form handling
  const form = useForm({
    resolver: zodResolver(checkoutSchema),
  });
  
  const billingSameAsShipping = form.watch("billingSameAsShipping");
  const paymentMethod = form.watch("paymentMethod");
  
  const onSubmit = async (data: CheckoutFormData) => {
    await submitOrder(data, { subtotal, courierTotal, discount, items });
  };
  
  return (
    (div className="container mx-auto px-4 py-8")
      (div className="grid grid-cols-1 lg:grid-cols-2 gap-8")
        {/* Left column: Forms */}
        (div className="space-y-6")
          (CheckoutShippingForm form={form} /)
          
          {!billingSameAsShipping && (
            (CheckoutBillingForm form={form} /)
          )}
          
          (CheckoutPaymentMethod
            form={form}
            availableMethods={PAYMENT_METHODS}
          /)
          
          (CheckoutDiscountCode
            value={discountCode}
            discount={discount}
            isValidating={isValidatingDiscount}
            onApply={applyDiscountCode}
          /)
        (/div)
        
        {/* Right column: Cart summary */}
        (div)
          (CheckoutCartSummary
            items={items}
            subtotal={subtotal}
            courierTotal={courierTotal}
            discount={discount}
            isValidating={isValidating}
            onRemoveItem={handleRemoveItem}
          /)
          
          (Button
            onClick={form.handleSubmit(onSubmit)}
            disabled={isProcessing || isValidating}
            className="w-full mt-6"
          )
            {isProcessing ? "Processing..." : `Pay \$\{formatCurrency(total)}`}
          (/Button)
        (/div)
      (/div)
    (/div)
  );
}

// src/components/checkout/checkout-cart-summary.tsx (~100 lines)
interface CheckoutCartSummaryProps {
  items: CartItem[];
  subtotal: number;
  courierTotal: number;
  discount: number;
  isValidating: boolean;
  onRemoveItem: (itemId: string) => void;
}

export function CheckoutCartSummary({ 
  items, subtotal, courierTotal, discount, isValidating, onRemoveItem 
}: CheckoutCartSummaryProps) {
  // Focused component with clear responsibilities
  return (
    (Card)
      (CardHeader)
        (CardTitle)Order Summary(/CardTitle)
      (/CardHeader)
      (CardContent)
        {/* Cart items display */}
        {/* Price breakdown */}
      (/CardContent)
    (/Card)
  );
}

// src/hooks/useCheckoutCart.ts (~80 lines)
export function useCheckoutCart(storeSlug: string, storeApiUrl: Function) {
  const [isValidating, setIsValidating] = useState(true);
  const [subtotal, setSubtotal] = useState(0);
  const [courierTotal, setCourierTotal] = useState(0);
  
  const items = useCart((state) => state.items);
  const removeItem = useCart((state) => state.removeItem);
  const getSubtotal = useCart((state) => state.getSubtotal);
  const getCourierTotal = useCart((state) => state.getCourierTotal);
  
  // Validation logic extracted here
  useEffect(() => {
    const validateCart = async () => {
      // Cart validation implementation
    };
    validateCart();
  }, [items, storeSlug]);
  
  return {
    items,
    isValidating,
    subtotal,
    courierTotal,
    handleRemoveItem: removeItem,
  };
}

Impact Assessment

  • Effort: High - Requires careful refactoring and comprehensive testing (~8-12 hours)
  • Risk: Medium - Complex component with business-critical logic (requires extensive testing)
  • Benefit: Very High - Dramatically improves maintainability, testability, and developer experience
  • Priority: Medium - Important for long-term maintainability, but not urgent

Related Files

  • src/app/store/[slug]/checkout/page.tsx (primary file)
  • Create new:
    • src/components/checkout/checkout-cart-summary.tsx
    • src/components/checkout/checkout-shipping-form.tsx
    • src/components/checkout/checkout-billing-form.tsx
    • src/components/checkout/checkout-payment-method.tsx
    • src/components/checkout/checkout-discount-code.tsx
    • src/hooks/useCheckoutCart.ts
    • src/hooks/useDiscountCode.ts
    • src/hooks/useCheckoutSubmit.ts

Testing Strategy

  1. Before refactoring:

    • Document current checkout flow behavior
    • Create end-to-end test for complete checkout process
    • Ensure existing functionality is fully tested
  2. During refactoring (iterative approach):

    • Test each extracted component in isolation
    • Test each custom hook with comprehensive unit tests
    • Maintain E2E test to catch regressions
  3. Component tests:

    • Test CheckoutCartSummary rendering and remove functionality
    • Test form components with valid/invalid inputs
    • Test payment method selection and availability
    • Test discount code validation and display
  4. Hook tests:

    • Test useCheckoutCart cart validation logic
    • Test useDiscountCode discount application
    • Test useCheckoutSubmit order creation flow
    • Mock API calls appropriately
  5. Integration tests:

    • Test complete checkout flow from cart to order creation
    • Test form validation across all steps
    • Test payment method switching
    • Test discount code application during checkout
  6. Manual testing:

    • Complete checkout with Cash on Delivery
    • Complete checkout with SSLCommerz payment
    • Test with discount codes
    • Test form validation errors
    • Test cart validation and invalid item removal
    • Test on mobile and desktop viewports

AI generated by Daily Codebase Analyzer - Semantic Function Extraction & Refactoring

  • expires on Mar 6, 2026, 1:56 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