-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
Problem
The OrderDetailClient component (src/components/order-detail-client.tsx) is 861 lines and violates single responsibility principle by mixing data fetching, business logic, and presentation:
Current Code Location
- File:
src/components/order-detail-client.tsx(861 lines) - Responsibilities:
- Data Fetching -
fetchOrder()(54 lines, lines 169-223) - Status Updates -
handleStatusUpdate()(48 lines, lines 231-278) - Tracking Updates -
handleTrackingUpdate()(48 lines, lines 281-330) - Refund Logic -
handleRefund()(34 lines, lines 332-365) - Invoice Download -
handleDownloadInvoice()(38 lines, lines 366-404) - Formatting Utilities -
formatCurrency(),formatAddress()(30+ lines) - Complex UI Rendering - 500+ lines of JSX with deeply nested cards
- Data Fetching -
Complexity Indicators:
- 10 distinct functions in one component
- 15+ state variables managed locally
- Multiple async operations with error handling
- Extensive conditional rendering based on order state
- Embedded utility functions that should be shared
Current Code Structure
// BEFORE: 861-line monolithic component
export function OrderDetailClient({ orderId, storeId }: Props) {
// State management (15+ state variables)
const [order, setOrder] = useState(Order | null)(null);
const [loading, setLoading] = useState(true);
const [updating, setUpdating] = useState(false);
const [newStatus, setNewStatus] = useState(OrderStatus)('PENDING');
const [trackingNumber, setTrackingNumber] = useState('');
const [trackingUrl, setTrackingUrl] = useState('');
const [organizationId, setOrganizationId] = useState(string | null)(null);
// ... 8 more state variables
// Data fetching (54 lines)
const fetchOrder = async () => {
// Complex fetch logic with error handling
// Address parsing logic
// Session fetching for organizationId
};
// Business logic handlers (130+ lines total)
const handleStatusUpdate = async () => { /* 48 lines */ };
const handleTrackingUpdate = async () => { /* 48 lines */ };
const handleRefund = async () => { /* 34 lines */ };
const handleDownloadInvoice = async () => { /* 38 lines */ };
// Formatting utilities (30+ lines)
const formatCurrency = (amount: number) => { /* ... */ };
const formatAddress = (address: Address | string) => { /* ... */ };
// Side effects
useEffect(() => {
fetchOrder();
}, [orderId, storeId]);
// 500+ lines of complex JSX rendering
return (
(div)
{/* Order header card */}
{/* Customer details card */}
{/* Order items table */}
{/* Shipping address card */}
{/* Billing address card */}
{/* Status update form */}
{/* Tracking update form */}
{/* Payment details card */}
{/* Pathao shipment panel */}
{/* Facebook order sync controls */}
{/* Refund dialog */}
(/div)
);
}Problems with Current Approach:
- Testing nightmare - Must mock 10+ functions and 15+ state variables
- Cannot reuse logic - Status update logic locked in component
- Hard to maintain - Change in one area affects entire 861-line file
- Poor performance - Re-renders entire UI on any state change
- Duplicate utilities -
formatCurrencyduplicated in 72+ files - Tight coupling - Business logic tightly coupled to presentation
Proposed Refactoring
Decompose into presentation components and custom hooks:
Benefits
- Testable business logic - Test hooks independently from UI
- Reusable across pages - Status update logic can be used in orders table
- Improved performance - Smaller components re-render less
- Better code organization - Separate concerns into focused modules
- Easier maintenance - Change status logic without touching UI
- Shared utilities - Extract formatting into shared lib
Suggested Approach
- Extract custom hook
useOrderManagement()for data fetching and state - Extract custom hook
useOrderActions()for business logic (status, tracking, refund) - Extract formatting utilities into
src/lib/formatters.ts - Create presentation components for each section (header, items, addresses)
- Compose in parent - OrderDetailClient becomes composition of components
Code Example
After: Custom Hook for Data Management
// src/hooks/useOrderManagement.ts
import { useState, useEffect } from 'react';
import { api } from '@/lib/api-client';
import type { Order } from '`@prisma/client`';
interface UseOrderManagementProps {
orderId: string;
storeId: string;
}
export function useOrderManagement({ orderId, storeId }: UseOrderManagementProps) {
const [order, setOrder] = useState(Order | null)(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(string | null)(null);
const fetchOrder = async () => {
try {
setLoading(true);
setError(null);
const data = await api.get(Order)(
`/api/orders/\$\{orderId}?storeId=\$\{storeId}`
);
// Parse addresses if needed
if (typeof data.shippingAddress === 'string') {
data.shippingAddress = JSON.parse(data.shippingAddress);
}
if (typeof data.billingAddress === 'string') {
data.billingAddress = JSON.parse(data.billingAddress);
}
setOrder(data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load order');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchOrder();
}, [orderId, storeId]);
return {
order,
loading,
error,
refetch: fetchOrder,
};
}After: Custom Hook for Order Actions
// src/hooks/useOrderActions.ts
import { useState } from 'react';
import { api } from '@/lib/api-client';
import { toast } from 'sonner';
import type { OrderStatus } from '`@prisma/client`';
interface UseOrderActionsProps {
orderId: string;
storeId: string;
onSuccess?: () => void;
}
export function useOrderActions({ orderId, storeId, onSuccess }: UseOrderActionsProps) {
const [updating, setUpdating] = useState(false);
const updateStatus = async (newStatus: OrderStatus) => {
try {
setUpdating(true);
await api.patch(`/api/orders/\$\{orderId}`, {
storeId,
newStatus,
});
toast.success('Order status updated successfully');
onSuccess?.();
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to update status');
throw error;
} finally {
setUpdating(false);
}
};
const updateTracking = async (trackingNumber: string, trackingUrl?: string) => {
try {
setUpdating(true);
await api.patch(`/api/orders/\$\{orderId}`, {
storeId,
trackingNumber,
trackingUrl,
});
toast.success('Tracking information updated');
onSuccess?.();
} catch (error) {
toast.error('Failed to update tracking');
throw error;
} finally {
setUpdating(false);
}
};
const processRefund = async (amount: number, reason: string) => {
try {
setUpdating(true);
await api.post(`/api/orders/\$\{orderId}/refund`, {
storeId,
amount,
reason,
});
toast.success(`Refund of \$\{amount} processed successfully`);
onSuccess?.();
} catch (error) {
toast.error('Failed to process refund');
throw error;
} finally {
setUpdating(false);
}
};
const downloadInvoice = async () => {
try {
const response = await fetch(`/api/orders/\$\{orderId}/invoice?storeId=\$\{storeId}`);
if (!response.ok) throw new Error('Failed to generate invoice');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `invoice-\$\{orderId}.pdf`;
a.click();
window.URL.revokeObjectURL(url);
toast.success('Invoice downloaded');
} catch (error) {
toast.error('Failed to download invoice');
}
};
return {
updating,
updateStatus,
updateTracking,
processRefund,
downloadInvoice,
};
}After: Shared Formatting Utilities
// src/lib/formatters.ts
/**
* Format currency amount with locale support
*/
export function formatCurrency(
amount: number,
currency: string = 'BDT',
locale: string = 'en-US'
): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(amount);
}
/**
* Format address object or JSON string into readable format
*/
export function formatAddress(
address: Address | string,
fallbackName?: string
): React.ReactNode {
const addr: Address = typeof address === 'string'
? JSON.parse(address)
: address;
const name = [addr.firstName, addr.lastName]
.filter(Boolean)
.join(' ') || fallbackName || '';
return (
(div className="text-sm")
{name && (div className="font-medium"){name}(/div)}
{addr.email && (div className="text-muted-foreground"){addr.email}(/div)}
(div){addr.address}(/div)
{addr.address2 && (div){addr.address2}(/div)}
(div)
{addr.city}, {addr.state} {addr.postalCode}
(/div)
(div){addr.country}(/div)
{addr.phone && (div)Phone: {addr.phone}(/div)}
(/div)
);
}After: Presentation Component
// src/components/order-detail/order-header.tsx
import { formatCurrency } from '@/lib/formatters';
import { Badge } from '@/components/ui/badge';
import { Card } from '@/components/ui/card';
interface OrderHeaderProps {
order: Order;
}
export function OrderHeader({ order }: OrderHeaderProps) {
return (
(Card className="p-6")
(div className="flex items-center justify-between")
(div)
<h1 className="text-2xl font-bold">
Order #{order.orderNumber}
</h1>
<p className="text-sm text-muted-foreground">
{format(new Date(order.createdAt), 'PPP p')}
</p>
(/div)
(div className="flex items-center gap-4")
(Badge variant={getStatusVariant(order.status)})
{order.status}
(/Badge)
(div className="text-right")
<p className="text-2xl font-bold">
{formatCurrency(order.total)}
</p>
<p className="text-sm text-muted-foreground">
{order.paymentStatus}
</p>
(/div)
(/div)
(/div)
(/Card)
);
}After: Simplified Main Component
// src/components/order-detail-client.tsx (now ~200 lines instead of 861)
import { useOrderManagement } from '@/hooks/useOrderManagement';
import { useOrderActions } from '@/hooks/useOrderActions';
import { OrderHeader } from './order-detail/order-header';
import { OrderItems } from './order-detail/order-items';
import { OrderAddresses } from './order-detail/order-addresses';
import { OrderStatusForm } from './order-detail/order-status-form';
import { OrderTrackingForm } from './order-detail/order-tracking-form';
export function OrderDetailClient({ orderId, storeId }: Props) {
// Data management hook
const { order, loading, error, refetch } = useOrderManagement({
orderId,
storeId
});
// Actions hook
const actions = useOrderActions({
orderId,
storeId,
onSuccess: refetch
});
if (loading) return (LoadingState /);
if (error) return (ErrorState message={error} /);
if (!order) return (NotFoundState /);
return (
(div className="space-y-6")
(OrderHeader order={order} /)
(div className="grid gap-6 md:grid-cols-2")
(OrderItems items={order.items} /)
(OrderAddresses
shipping={order.shippingAddress}
billing={order.billingAddress}
/)
(/div)
(OrderStatusForm
currentStatus={order.status}
onUpdate={actions.updateStatus}
updating={actions.updating}
/)
(OrderTrackingForm
trackingNumber={order.trackingNumber}
trackingUrl={order.trackingUrl}
onUpdate={actions.updateTracking}
updating={actions.updating}
/)
(Button onClick={actions.downloadInvoice})
Download Invoice
(/Button)
(/div)
);
}Result:
- Main component reduced from 861 to ~200 lines (77% reduction)
- Business logic testable - Test hooks without rendering UI
- Reusable hooks - Status update logic can be used in other pages
- Shared utilities - formatCurrency used across 72+ files
- Better performance - Smaller components = less re-rendering
Impact Assessment
- Effort: Medium-High - Initial refactor (8-12 hours), testing (4-6 hours)
- Risk: Medium - Large refactor but with clear structure
- Benefit: High - 77% code reduction, testability, reusability
- Priority: High - Template for refactoring other large components
Migration Strategy:
- Extract hooks first (can coexist with existing code)
- Create presentation components
- Update OrderDetailClient to use new structure
- Add tests for hooks
- Remove old code
Related Files
Similar patterns to refactor after this:
src/components/product-edit-form.tsx(886 lines)src/app/store/[slug]/checkout/page.tsx(827 lines)src/components/orders-table.tsx(981 lines)src/components/integrations/facebook/dashboard.tsx(1,106 lines)
Testing Strategy
Hook Tests:
// src/hooks/__tests__/useOrderActions.test.ts
import { renderHook, act } from '`@testing-library/react`';
import { useOrderActions } from '../useOrderActions';
import { api } from '@/lib/api-client';
jest.mock('@/lib/api-client');
describe('useOrderActions', () => {
it('should update order status', async () => {
const onSuccess = jest.fn();
const { result } = renderHook(() =>
useOrderActions({ orderId: '123', storeId: 'store-1', onSuccess })
);
await act(async () => {
await result.current.updateStatus('SHIPPED');
});
expect(api.patch).toHaveBeenCalledWith('/api/orders/123', {
storeId: 'store-1',
newStatus: 'SHIPPED',
});
expect(onSuccess).toHaveBeenCalled();
});
it('should handle update errors', async () => {
api.patch.mockRejectedValueOnce(new Error('Network error'));
const { result } = renderHook(() =>
useOrderActions({ orderId: '123', storeId: 'store-1' })
);
await expect(async () => {
await act(async () => {
await result.current.updateStatus('SHIPPED');
});
}).rejects.toThrow('Network error');
});
});Component Tests:
// src/components/order-detail/__tests__/order-header.test.tsx
import { render, screen } from '`@testing-library/react`';
import { OrderHeader } from '../order-header';
describe('OrderHeader', () => {
it('should render order number and total', () => {
const order = {
orderNumber: 'ORD-12345',
total: 99.99,
status: 'PENDING',
createdAt: new Date(),
};
render((OrderHeader order={order} /));
expect(screen.getByText('Order #ORD-12345')).toBeInTheDocument();
expect(screen.getByText(/99\.99/)).toBeInTheDocument();
});
});AI generated by Daily Codebase Analyzer - Semantic Function Extraction & Refactoring
- expires on Mar 3, 2026, 2:05 PM UTC
Reactions are currently unavailable
Metadata
Metadata
Assignees
Type
Projects
Status
Backlog