Skip to content

[Refactoring] Components: Extract checkout form validation and business logic from page component (1039 lines) #228

@syed-reza98

Description

@syed-reza98

Problem

The checkout page component at src/app/store/[slug]/checkout/page.tsx is 1,039 lines and violates the separation of concerns principle by mixing:

  • UI presentation logic (JSX rendering, form layout)
  • Form state management (55+ state variables and hooks)
  • Business logic (price calculations, discount validation, shipping cost calculation)
  • API integration (order creation, discount application)
  • Complex validation (billing/shipping address, payment methods, Pathao integration)

This makes the component:

  • Difficult to test - business logic is coupled with React rendering
  • Hard to maintain - changes require understanding the entire 1000+ line component
  • Not reusable - validation and calculation logic can't be used elsewhere
  • Slow to render - all logic runs in the browser on every render

Current Code Location

  • File: src/app/store/[slug]/checkout/page.tsx (lines 1-1039)
  • Component: CheckoutPage (client component)
  • Complexity: Very High
  • Hook count: 14 conditional statements, 55+ hooks/functions

Proposed Refactoring

Benefits

  • Improved testability: Business logic can be tested without React
  • Better performance: Move calculations to server components or hooks
  • Code reusability: Validation and calculations can be used in other contexts
  • Easier maintenance: Clear separation makes changes more predictable
  • Type safety: Extracted functions have explicit type signatures

Suggested Approach

  1. Extract form schema and validationsrc/lib/validation/checkout-validation.ts

    • Move checkoutSchema (lines 57-113)
    • Extract validation refinements into testable functions
  2. Extract business logic hookssrc/hooks/useCheckoutCalculations.ts

    • Price calculations (subtotal, shipping, taxes, discounts, total)
    • Discount code validation and application
    • Shipping cost calculation based on location
  3. Extract order processingsrc/lib/services/checkout.service.ts (may exist)

    • Move API integration logic
    • Order creation workflow
    • Error handling and retry logic
  4. Extract form componentssrc/components/checkout/

    • CheckoutCustomerInfo.tsx - Email, name, phone fields
    • CheckoutShippingAddress.tsx - Address form with Pathao integration
    • CheckoutBillingAddress.tsx - Optional billing address
    • CheckoutPaymentMethod.tsx - Payment method selection
    • CheckoutOrderSummary.tsx - Order summary, discount, total
    • CheckoutActions.tsx - Submit button and validation
  5. Keep page component lean (target: <200 lines)

    • Orchestrate form submission
    • Handle navigation and routing
    • Compose extracted components

Code Example

Before:

// src/app/store/[slug]/checkout/page.tsx (1039 lines)
"use client";

export default function CheckoutPage() {
  // 55+ lines of state and hooks
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [discount, setDiscount] = useState(Discount | null)(null);
  const [discountCode, setDiscountCode] = useState("");
  const [isApplyingDiscount, setIsApplyingDiscount] = useState(false);
  // ... 50+ more state variables
  
  // 100+ lines of validation schema
  const checkoutSchema = z.object({
    email: z.string().email("Please enter a valid email address"),
    firstName: z.string().min(1, "First name is required"),
    // ... 50+ more fields
  }).refine((data) => {
    // Complex validation logic
  });
  
  // 200+ lines of business logic
  const calculateSubtotal = () => {
    return cartItems.reduce((sum, item) => {
      const effectivePrice = item.variant
        ? calculateEffectivePrice(item.variant)
        : calculateEffectivePrice(item.product);
      return sum + effectivePrice * item.quantity;
    }, 0);
  };
  
  const calculateShipping = () => {
    // Complex shipping calculation
  };
  
  const calculateDiscount = () => {
    // Complex discount calculation
  };
  
  // 150+ lines of form handlers
  const onSubmit = async (data: CheckoutFormValues) => {
    // Complex order creation logic
  };
  
  const handleApplyDiscount = async () => {
    // Discount application logic
  };
  
  // 500+ lines of JSX
  return (
    (form onSubmit={handleSubmit(onSubmit)})
      {/* Massive form with deeply nested components */}
    (/form)
  );
}

After:

// src/lib/validation/checkout-validation.ts (~150 lines)
import { z } from 'zod';
import { emailSchema, phoneSchema } from '@/lib/validation/schemas';

export const checkoutSchema = z.object({
  email: emailSchema,
  firstName: z.string().min(1, "First name is required"),
  lastName: z.string().min(1, "Last name is required"),
  phone: phoneSchema,
  deliveryLocation: z.enum(["inside_dhaka", "outside_dhaka"]),
  shippingAddress: z.string().min(5, "Address is required"),
  // ... rest of schema
}).refine(validateBillingAddress, {
  message: "Billing address is required when different from shipping",
});

export function validateBillingAddress(data: CheckoutFormData): boolean {
  if (!data.billingSameAsShipping) {
    return !!(data.billingAddress && data.billingCity && data.billingState);
  }
  return true;
}

export function validatePathaoFields(data: CheckoutFormData): boolean {
  // Validation logic
}

export type CheckoutFormData = z.infer(typeof checkoutSchema);

// src/hooks/useCheckoutCalculations.ts (~200 lines)
import { CartItem } from '@/lib/stores/cart-store';
import { Discount } from '`@prisma/client`';

export function useCheckoutCalculations(
  cartItems: CartItem[],
  discount: Discount | null,
  deliveryLocation: string
) {
  const subtotal = useMemo(() => {
    return cartItems.reduce((sum, item) => {
      const effectivePrice = item.variant
        ? calculateEffectivePrice(item.variant)
        : calculateEffectivePrice(item.product);
      return sum + effectivePrice * item.quantity;
    }, 0);
  }, [cartItems]);

  const shippingCost = useMemo(() => {
    return calculateShippingCost(cartItems, deliveryLocation);
  }, [cartItems, deliveryLocation]);

  const discountAmount = useMemo(() => {
    if (!discount) return 0;
    return calculateDiscountAmount(subtotal, discount);
  }, [subtotal, discount]);

  const total = subtotal + shippingCost - discountAmount;

  return {
    subtotal,
    shippingCost,
    discountAmount,
    total,
  };
}

function calculateEffectivePrice(item: Product | ProductVariant): number {
  // Extract price calculation logic
}

function calculateShippingCost(items: CartItem[], location: string): number {
  // Extract shipping calculation
}

function calculateDiscountAmount(subtotal: number, discount: Discount): number {
  // Extract discount calculation
}

// src/components/checkout/CheckoutCustomerInfo.tsx (~80 lines)
import { UseFormReturn } from 'react-hook-form';
import { CheckoutFormData } from '@/lib/validation/checkout-validation';

interface CheckoutCustomerInfoProps {
  form: UseFormReturn(CheckoutFormData);
}

export function CheckoutCustomerInfo({ form }: CheckoutCustomerInfoProps) {
  return (
    (Card)
      (CardHeader)
        (CardTitle)Customer Information(/CardTitle)
      (/CardHeader)
      (CardContent)
        {/* Focused component - just customer fields */}
        (div className="space-y-4")
          (div)
            (Label htmlFor="email")Email(/Label)
            (Input {...form.register('email')} /)
            {form.formState.errors.email && (
              <p className="text-sm text-red-600">{form.formState.errors.email.message}</p>
            )}
          (/div)
          {/* ... other customer fields */}
        (/div)
      (/CardContent)
    (/Card)
  );
}

// src/components/checkout/CheckoutOrderSummary.tsx (~150 lines)
import { useCheckoutCalculations } from '@/hooks/useCheckoutCalculations';

interface CheckoutOrderSummaryProps {
  cartItems: CartItem[];
  discount: Discount | null;
  deliveryLocation: string;
  onApplyDiscount: (code: string) => Promise(void);
  onRemoveDiscount: () => void;
}

export function CheckoutOrderSummary({
  cartItems,
  discount,
  deliveryLocation,
  onApplyDiscount,
  onRemoveDiscount,
}: CheckoutOrderSummaryProps) {
  const { subtotal, shippingCost, discountAmount, total } = useCheckoutCalculations(
    cartItems,
    discount,
    deliveryLocation
  );

  return (
    (Card)
      (CardHeader)
        (CardTitle)Order Summary(/CardTitle)
      (/CardHeader)
      (CardContent)
        {/* Focused component - just summary and calculations */}
        (div className="space-y-2")
          (div className="flex justify-between")
            (span)Subtotal(/span)
            (span)${subtotal.toFixed(2)}(/span)
          (/div)
          {/* ... rest of summary */}
        (/div)
      (/CardContent)
    (/Card)
  );
}

// src/app/store/[slug]/checkout/page.tsx (~180 lines - much cleaner!)
"use client";

import { useCheckoutCalculations } from '@/hooks/useCheckoutCalculations';
import { checkoutSchema, CheckoutFormData } from '@/lib/validation/checkout-validation';
import { CheckoutCustomerInfo } from '@/components/checkout/CheckoutCustomerInfo';
import { CheckoutShippingAddress } from '@/components/checkout/CheckoutShippingAddress';
import { CheckoutOrderSummary } from '@/components/checkout/CheckoutOrderSummary';
import { CheckoutActions } from '@/components/checkout/CheckoutActions';

export default function CheckoutPage() {
  const router = useRouter();
  const params = useParams();
  const { items: cartItems, clearCart } = useCart();
  const [discount, setDiscount] = useState(Discount | null)(null);
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  const form = useForm(CheckoutFormData)({
    resolver: zodResolver(checkoutSchema),
    defaultValues: { /* ... */ },
  });
  
  const { subtotal, shippingCost, discountAmount, total } = useCheckoutCalculations(
    cartItems,
    discount,
    form.watch('deliveryLocation')
  );
  
  const handleApplyDiscount = async (code: string) => {
    // Simplified discount logic
  };
  
  const onSubmit = async (data: CheckoutFormData) => {
    // Simplified order creation
  };
  
  return (
    (div className="container max-w-6xl py-8")
      (form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-8 lg:grid-cols-3")
        (div className="lg:col-span-2 space-y-6")
          (CheckoutCustomerInfo form={form} /)
          (CheckoutShippingAddress form={form} /)
        (/div)
        (div className="lg:col-span-1")
          (CheckoutOrderSummary
            cartItems={cartItems}
            discount={discount}
            deliveryLocation={form.watch('deliveryLocation')}
            onApplyDiscount={handleApplyDiscount}
            onRemoveDiscount={() =) setDiscount(null)}
          />
          (CheckoutActions
            isSubmitting={isSubmitting}
            isValid={form.formState.isValid}
            total={total}
          /)
        (/div)
      (/form)
    (/div)
  );
}

Impact Assessment

  • Effort: High - Estimated 2-3 days to extract and test
  • Risk: Medium - Requires careful testing of business logic extraction
  • Benefit: Very High - One of the largest, most complex components in the codebase
  • Priority: High - Critical user-facing feature that needs to be maintainable

Related Files

  • src/app/store/[slug]/checkout/page.tsx (1039 lines - to be refactored)
  • src/lib/stores/cart-store.ts (cart state management)
  • src/components/shipping/pathao-address-selector.tsx (Pathao integration)
  • src/lib/services/checkout.service.ts (may exist for order processing)

Testing Strategy

  1. Unit tests for business logic

    • Test calculateEffectivePrice() with various discount scenarios
    • Test calculateShippingCost() with different locations
    • Test calculateDiscountAmount() with percentage and fixed discounts
    • Test validation functions independently
  2. Integration tests for hooks

    • Test useCheckoutCalculations with various cart states
    • Verify calculations update correctly when dependencies change
  3. Component tests

    • Test each extracted component in isolation
    • Verify form validation and error messages
    • Test Pathao address selector integration
  4. E2E tests

    • Test complete checkout flow
    • Verify order creation with different payment methods
    • Test discount code application

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

  • expires on Feb 27, 2026, 8:37 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