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 responsibilityFlat 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 hooksDoes not share _lib/api/ or _lib/contexts/. See Extensions recipe for the extension pattern.
Rules
- Never transform, filter, or sort data in JSX — always use
useMemofor data preparation - Follow the component structure order: imports → types → helpers → hooks → useMemo → useCallback → early returns → render
- Use context over props — no prop drilling through intermediate components
- Wrap all event handlers in
useCallbackwith explicit dependency arrays - Helper functions go outside the component body — pure, testable, no hooks
- No
anytypes — all props typed with interfaces, types imported from contexttypes.ts