Skip to Content
FrontendComponent Patterns

Component Patterns

Zero-tolerance rules and standard patterns for React components in django-cfg apps.

For Context architecture see State Management. For UI component library see UI Components recipe.

The useMemo Data Preparation Rule

Never transform data in JSX. Always use useMemo.

This is the top zero-tolerance rule across all projects.

// WRONG — logic in JSX {users?.filter(u => u.active)?.map(u => <div>{u.name}</div>)} // CORRECT — prepare data first const activeUsers = useMemo(() => { if (!users) return []; return users .filter(user => user.active) .map(user => ({ ...user, displayName: `${user.firstName} ${user.lastName}`, statusText: user.isActive ? 'Active' : 'Inactive', })); }, [users]); {activeUsers.map(user => <div key={user.id}>{user.displayName}</div>)}

Multiple useMemo for Different Concerns

// Filtered data const filteredOrders = useMemo(() => { ... }, [orders, filters]); // Stats computed from filtered data const stats = useMemo(() => ({ total: filteredOrders.length, totalValue: filteredOrders.reduce((sum, o) => sum + o.total, 0), }), [filteredOrders]); // Chart data const chartData = useMemo(() => filteredOrders.map(o => ({ date: o.date, value: o.total })), [filteredOrders]);

Component Structure Order

Every component follows this internal order:

// 1. Imports import { useMemo, useCallback } from 'react'; // 2. Types interface UserCardProps { user: User; onSelect: (id: string) => void; } // 3. Helper functions (OUTSIDE component — pure, testable) function formatUserRole(role: UserRole): string { ... } function getUserStatusColor(status: string): string { ... } // 4. Component body export function UserCard({ user, onSelect }: UserCardProps) { // 4a. Hooks (state, context, custom) const { isAdmin } = useAuth(); // 4b. useMemo for derived data const displayData = useMemo(() => ({ name: `${user.first} ${user.last}`, roleLabel: formatUserRole(user.role), statusColor: getUserStatusColor(user.status), }), [user]); // 4c. useCallback for event handlers const handleSelect = useCallback(() => { onSelect(user.id); }, [user.id, onSelect]); // 4d. Early returns (loading, error, empty) if (!user) return null; // 4e. Main render return ( <Card onClick={handleSelect}> <span className={displayData.statusColor}>{displayData.name}</span> <Badge>{displayData.roleLabel}</Badge> </Card> ); }

Context Over Props

No prop drilling. Components access context directly:

// WRONG — prop drilling <Layout user={user}> <Sidebar user={user}> <UserMenu user={user} /> </Sidebar> </Layout> // CORRECT — components access context directly <AuthProvider> <Layout> <Sidebar> <UserMenu /> {/* calls useAuth() internally */} </Sidebar> </Layout> </AuthProvider>

useCallback for Event Handlers

All event handlers use useCallback:

// WRONG <Button onClick={() => handleClick(id)}>Click</Button> // CORRECT const handleButtonClick = useCallback(() => { handleClick(id); }, [id, handleClick]); <Button onClick={handleButtonClick}>Click</Button>

Component Decomposition Patterns

Page → View → Features

page.tsx (Server) → generateMetadata() → SSR data fetch → renders <FeatureView initialData={...} /> FeatureView.tsx ('use client') → receives data props → wraps with providers if needed → composes feature components Feature components → consume context directly → each has single responsibility

Flat Composition

When no sub-contexts are needed, use a flat layout:

// VehicleDetailView.tsx <div> <VehicleBreadcrumb /> <VehicleGallery /> <VehicleHeader /> <VehicleSpecs /> <VehicleDescription /> <VehicleFeatures /> </div>

Dialog Wrapper Pattern

Separate dialog state (open/close) from content logic:

// EditDialog.tsx — handles open/close export function EditDialog({ open, onOpenChange }) { return ( <Dialog open={open} onOpenChange={onOpenChange}> <DialogContent> <EditForm onSuccess={() => onOpenChange(false)} /> </DialogContent> </Dialog> ); } // EditForm.tsx — handles actual editing logic export function EditForm({ onSuccess }) { const form = useForm(); // ... form logic only, no dialog concerns }

State Machine for Complex Flows

type TransferState = 'idle' | 'initiating' | 'uploading' | 'streaming' | 'completed' | 'failed' | 'cancelled'; // Render with switch switch (state) { case 'idle': return <StartButton />; case 'uploading': return <ProgressBar percent={progress} />; case 'completed': return <SuccessMessage />; case 'failed': return <RetryButton />; }

Dual Layout Support

Support grid and list layouts via a prop:

interface VehicleCardProps { vehicle: VehicleList; layout: 'grid' | 'list'; } export function VehicleCard({ vehicle, layout }: VehicleCardProps) { if (layout === 'list') return <ListLayout vehicle={vehicle} />; return <GridLayout vehicle={vehicle} />; }

Self-Contained Feature Modules

Features with separate backend modules can be fully self-contained:

private/payments/src/ ├── api/generated/ext_payments/ # Own generated API ├── contexts/WalletContext.tsx # Own context ├── contexts/types.ts # Own type re-exports ├── components/ # Own components └── hooks/ # Own hooks

Does not share _lib/api/ or _lib/contexts/. See Extensions recipe for the extension pattern.

Rules

  1. Never transform, filter, or sort data in JSX — always use useMemo for data preparation
  2. Follow the component structure order: imports → types → helpers → hooks → useMemo → useCallback → early returns → render
  3. Use context over props — no prop drilling through intermediate components
  4. Wrap all event handlers in useCallback with explicit dependency arrays
  5. Helper functions go outside the component body — pure, testable, no hooks
  6. No any types — all props typed with interfaces, types imported from context types.ts
Last updated on