@jjdenhertog/ai-driven-development
Version:
AI-driven development workflow with learning capabilities for Claude
1,192 lines (984 loc) • 33.1 kB
Markdown
---
name: "Component Patterns Guide"
category: "frontend"
version: "1.0.0"
dependencies: ["technology-stack", "styling", "state-management"]
tags: ["react", "components", "typescript", "performance", "patterns"]
priority: "critical"
ai-instructions:
- "ALWAYS use TypeScript with readonly props"
- "ALWAYS use arrow functions with React.FC type annotation"
- "MUST memoize callbacks passed as props using useCallback"
- "MUST use useMemo for expensive computations and filtering/sorting"
- "PREFER composition over complex component hierarchies"
- "USE React Context when prop drilling exceeds 2-3 levels"
- "CLEAN custom props before spreading to DOM elements"
- "NEVER use class components - only functional components"
---
# Component Patterns Guide
This guide outlines our component architecture patterns, composition strategies, and best practices for building consistent, performant React components in our Next.js application.
<ai-context>
This preference file defines how to structure and implement React components.
All components must follow these patterns for consistency, type safety, and performance.
Components are organized by feature in src/features/ with shared UI in src/components/.
</ai-context>
## Table of Contents
1. [Component Philosophy](#component-philosophy)
2. [Composition Patterns](#composition-patterns)
3. [State Management Rules](#state-management-rules)
4. [Component Categories](#component-categories)
5. [Props Patterns](#props-patterns)
6. [Conditional Rendering](#conditional-rendering)
7. [Performance Optimization](#performance-optimization)
8. [Common Component Abstractions](#common-component-abstractions)
---
## Component Philosophy
<ai-rules category="component-basics">
<rule id="typescript-required" priority="critical">
<condition>Creating any component</condition>
<action>Use TypeScript with explicit type annotations</action>
<validation>File has .tsx extension and typed props</validation>
</rule>
<rule id="functional-only" priority="critical">
<condition>Defining React components</condition>
<action>Use arrow functions with React.FC type</action>
<validation>No class components, only const = () =></validation>
<example>export const Component: React.FC<Props> = ({ prop }) => {}</example>
</rule>
<rule id="readonly-props" priority="critical">
<condition>Defining component props interface</condition>
<action>Mark all props as readonly</action>
<validation>All interface properties have readonly modifier</validation>
</rule>
</ai-rules>
Our component architecture follows these core principles:
- **Type Safety First**: All props use TypeScript with `readonly` modifiers
- **Controlled Components**: Form inputs are always controlled
- **Feature-Based Organization**: Components organized by feature with shared UI components
- **Composition Over Inheritance**: Use composition patterns for flexibility
- **Performance by Default**: Strategic use of `useCallback` and `useMemo`
## Composition Patterns
### 1. Render Props Pattern
Use render props when you need flexible rendering of complex content.
**When to use:**
- Custom list item rendering
- Flexible input components
- Tree structures with varying node types
**Example:**
```tsx
// ✅ Good - Flexible rendering
<BAutocomplete
renderOption={(option, props) => (
<li {...props}>
<Avatar src={option.avatar} />
{option.label}
</li>
)}
renderInput={(params) => (
<TextField {...params} label="Select user" />
)}
/>
// ✅ Good - Tree with multiple node types
<Tree
render={(node, options) => {
switch (node.data.type) {
case "folder":
return <TreeNodeFolder node={node} {...options} />
case "version":
return <TreeNodeVersion node={node} {...options} />
}
}}
/>
```
### 2. Prop Spreading with Cleanup
<ai-rules category="prop-spreading">
<rule id="clean-custom-props" priority="high">
<condition>Spreading props to DOM elements</condition>
<action>Remove custom props before spreading</action>
<validation>No console warnings about unknown DOM properties</validation>
</rule>
</ai-rules>
<code-template id="component-with-prop-cleanup">
<description>Component that cleans custom props before spreading</description>
<variables>
<var name="COMPONENT_NAME" type="string" example="BTextField" />
<var name="BASE_COMPONENT" type="string" example="TextField" />
<var name="CUSTOM_PROP" type="string" example="onPressEnter" />
</variables>
<template>
export const ${COMPONENT_NAME} = (props: ${COMPONENT_NAME}Props) => {
const { ${CUSTOM_PROP}, validation, ...baseProps } = props;
// Handle custom props
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter' && ${CUSTOM_PROP}) {
${CUSTOM_PROP}();
}
}, [${CUSTOM_PROP}]);
return <${BASE_COMPONENT} {...baseProps} onKeyDown={handleKeyDown} />;
}
</template>
</code-template>
Always clean custom props before spreading to avoid DOM warnings.
**Pattern:**
```tsx
// ✅ Good - Clean custom props
export const BTextField = (props: BTextFieldProps) => {
const textFieldProps = { ...props };
delete textFieldProps.onPressEnter;
delete textFieldProps.validation;
return <TextField {...textFieldProps} />
}
// ❌ Bad - Spreading all props
export const BadTextField = (props: CustomProps) => {
return <TextField {...props} /> // onPressEnter will cause DOM warning
}
```
### 3. Children Composition
Use children for flexible content composition in layout components.
**Pattern:**
```tsx
// ✅ Good - Flexible layout
export type MainLayoutProps = {
readonly children: React.ReactNode
readonly button?: React.ReactNode
readonly extraNavigation?: ReactNode
}
// ✅ Good - Provider pattern
interface ErrorProviderProps {
readonly children?: React.ReactNode | React.ReactNode[]
}
```
## State Management Rules
<ai-decision-tree id="state-management-choice">
<question>Does the state need to be shared across components?</question>
<yes>
<question>Is it server state (from API)?</question>
<yes>
<answer>Use TanStack Query</answer>
<template>tanstack-query-hook</template>
</yes>
<no>
<question>Is it app-wide state?</question>
<yes>
<answer>Use Zustand store</answer>
<template>zustand-store</template>
</yes>
<no>
<question>Does prop drilling exceed 2-3 levels?</question>
<yes>
<answer>Use React Context</answer>
<template>context-provider</template>
</yes>
<no>
<answer>Pass as props</answer>
</no>
</no>
</no>
</yes>
<no>
<answer>Use useState in component</answer>
<template>local-state</template>
</no>
</ai-decision-tree>
### When to Use Local State
Use `useState` for:
- Form input values
- UI state (open/closed, loading, hover)
- Temporary data before submission
- Component-specific state that doesn't need sharing
**Example:**
```tsx
// ✅ Good - UI state
const [focussed, setFocussed] = useState<boolean>(false)
const [error, setError] = useState<boolean>(false)
// ✅ Good - Form state
const [formData, setFormData] = useState<FormData>({
name: '',
email: ''
})
```
### When to Use Context
Create a context when:
- Data needs to be accessed by many components
- Prop drilling exceeds 2-3 levels
- Managing cross-cutting concerns (errors, auth, theme)
**Context Pattern:**
```tsx
// 1. Define context with noOp defaults
export const ErrorContext = createContext<ErrorContextType>({
showError: () => {},
showWarning: () => {},
})
// 2. Create provider with actual implementation
export function ErrorProvider({ children }: { children: ReactNode }) {
const [errors, setErrors] = useState<Error[]>([])
const showError = useCallback((message: string) => {
setErrors(prev => [...prev, { message, type: 'error' }])
}, [])
return (
<ErrorContext.Provider value={{ showError, showWarning }}>
{children}
</ErrorContext.Provider>
)
}
// 3. Use with hook
export const useError = () => {
const context = useContext(ErrorContext)
if (!context) {
throw new Error('useError must be used within ErrorProvider')
}
return context
}
```
### Prop Drilling Threshold
**2-3 Levels Maximum** before considering context or composition.
```tsx
// ✅ Good - 2 levels is acceptable
<Dashboard user={user}>
<UserProfile user={user} />
</Dashboard>
// ⚠️ Warning - 3 levels, consider alternatives
<Dashboard user={user}>
<ProfileSection user={user}>
<UserAvatar user={user} />
</ProfileSection>
</Dashboard>
// ❌ Bad - Too much drilling, use context
<App user={user}>
<Dashboard user={user}>
<ProfileSection user={user}>
<UserDetails user={user}>
<UserAvatar user={user} />
</UserDetails>
</ProfileSection>
</Dashboard>
</App>
```
## Component Categories
### Container Components
**Purpose:** Handle data fetching, state management, and business logic.
**Characteristics:**
- Fetch data from APIs
- Manage complex state
- Provide data to children
- Handle side effects
**Example:**
```tsx
export default function FlightConfigurator({ flightId }: Props) {
// Data fetching
const [loading, setLoading] = useState(true)
const { load: loadComponents, components } = useComponents()
const { load: loadConfig, save: saveConfig } = useConfig(flightId)
// Business logic
useEffect(() => {
Promise.all([
loadComponents(),
loadConfig()
]).finally(() => setLoading(false))
}, [flightId])
// Render presentational components
return (
<ConfiguratorContext.Provider value={contextValue}>
<ConfiguratorLayout>
<ComponentList components={components} />
<ConfigPanel config={config} />
</ConfiguratorLayout>
</ConfiguratorContext.Provider>
)
}
```
### Presentational Components
**Purpose:** Display UI based on props, no business logic.
**Characteristics:**
- Props-driven rendering
- No data fetching
- Emit events through callbacks
- Reusable across features
**Example:**
```tsx
export type BTextFieldProps = TextFieldProps & {
readonly onPressEnter?: () => void
readonly validation?: "name" | "email" | "phonenumber"
}
export const BTextField = (props: BTextFieldProps) => {
const [error, setError] = useState(false)
// Only UI logic
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter' && props.onPressEnter) {
props.onPressEnter()
}
}, [props.onPressEnter])
// Pure rendering
return (
<TextField
{...props}
error={error}
onKeyDown={handleKeyDown}
/>
)
}
```
### Layout Components
**Purpose:** Define page structure and common UI patterns.
**Example:**
```tsx
export type MainLayoutProps = {
readonly children: React.ReactNode
readonly button?: React.ReactNode
readonly showBreadcrumbs?: boolean
}
export function MainLayout({ children, button, showBreadcrumbs = true }: MainLayoutProps) {
return (
<Box>
<Navigation />
{showBreadcrumbs && <Breadcrumbs />}
<Container>
{children}
</Container>
{button && (
<FloatingActionButton>
{button}
</FloatingActionButton>
)}
</Box>
)
}
```
### Provider Components
**Purpose:** Provide global state and functionality through context.
**Pattern:**
```tsx
interface ProviderProps {
readonly children: React.ReactNode
}
export function ThemeProvider({ children }: ProviderProps) {
const [theme, setTheme] = useState<Theme>('light')
const toggleTheme = useCallback(() => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}, [])
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
```
## Props Patterns
### Interface Definitions
Always use readonly props with proper TypeScript types.
**Pattern:**
```tsx
// ✅ Good - Readonly props with clear types
export type ComponentProps = {
readonly id: string
readonly name: string
readonly onSelect?: (id: string) => void
readonly options?: ReadonlyArray<Option>
}
// ✅ Good - Extending existing types
export type BTextFieldProps = TextFieldProps & {
readonly onPressEnter?: () => void
readonly validation?: "name" | "email" | "phonenumber"
}
// ❌ Bad - Mutable props
type BadProps = {
items: Item[] // Should be readonly
onChange: Function // Too generic
}
```
### Default Props Pattern
Use destructuring with defaults for optional props.
```tsx
// ✅ Good - Clear defaults
export function FileUploader(props: FileUploaderProps) {
const {
maxFileSize = 5,
allowMultiple = false,
label = "Select files",
errors = [],
onComplete
} = props
}
// ✅ Good - Object defaults
export function BSelect(props: BSelectProps) {
const {
box = {}, // Empty object default
mb = 1, // Numeric default
variant = "outlined" // String default
} = props
}
```
### Props Documentation
Document complex props with JSDoc comments.
```tsx
export type FileUploaderProps = {
/** Maximum file size in MB. Default: 5 */
readonly maxFileSize?: number
/** Allow multiple file selection. Default: false */
readonly allowMultiple?: boolean
/** Callback fired when upload completes */
readonly onComplete?: (files: UploadedFile[]) => void
/** Accepted file types (MIME types or extensions) */
readonly accept?: string | string[]
}
```
## Conditional Rendering
### Pattern Priority
1. **Early returns** for invalid states
2. **&& operator** for simple conditions
3. **Ternary operator** for either/or rendering
4. **Helper functions** for complex logic
### Examples
```tsx
// ✅ Good - Early return for invalid state
if (!data) {
return <EmptyState />
}
// ✅ Good - && for optional rendering
return (
<Card>
{title && <CardHeader>{title}</CardHeader>}
<CardContent>{content}</CardContent>
{actions && <CardActions>{actions}</CardActions>}
</Card>
)
// ✅ Good - Ternary for loading states
return loading ? (
<Box p={3}>
<CircularProgress />
</Box>
) : (
<DataGrid data={data} />
)
// ✅ Good - Extract complex conditions
const renderContent = () => {
if (error) return <ErrorMessage error={error} />
if (loading) return <Skeleton />
if (!data) return <EmptyState />
return <DataDisplay data={data} />
}
return <Container>{renderContent()}</Container>
```
### Anti-patterns to Avoid
```tsx
// ❌ Bad - Nested ternaries
return loading ? <Loader /> : error ? <Error /> : data ? <Data /> : <Empty />
// ❌ Bad - Complex logic in JSX
return (
<div>
{user && user.permissions && user.permissions.includes('admin') &&
!user.suspended && feature.enabled && <AdminPanel />}
</div>
)
```
## Performance Optimization
<ai-rules category="performance">
<rule id="usecallback-required" priority="critical">
<condition>Creating event handler passed as prop</condition>
<action>Wrap in useCallback with correct dependencies</action>
<validation>All functions passed to child components are memoized</validation>
</rule>
<rule id="usememo-filtering" priority="high">
<condition>Filtering or sorting arrays in render</condition>
<action>Wrap operation in useMemo</action>
<validation>No array operations directly in render body</validation>
</rule>
<rule id="context-value-memoization" priority="critical">
<condition>Creating context provider value</condition>
<action>Always memoize with useMemo</action>
<validation>Context value is wrapped in useMemo</validation>
</rule>
</ai-rules>
### useCallback Rules
Always use `useCallback` for:
- Event handlers passed as props
- Dependencies of other hooks
- Functions in dependency arrays
```tsx
// ✅ Good - Memoized event handlers
const handleChange = useCallback((value: string) => {
setValue(value)
if (onChange) {
onChange(value)
}
}, [onChange]) // Include all external dependencies
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter' && onPressEnter) {
onPressEnter()
}
}, [onPressEnter])
```
### useMemo Guidelines
Use `useMemo` for:
- Expensive computations
- Reference equality for objects/arrays
- Computed values used in dependency arrays
- Filtering and sorting operations
- Context values to prevent cascading re-renders
- Derived state from props or other state
```tsx
// ✅ Good - Expensive computation
const sortedData = useMemo(() => {
return [...data].sort((a, b) => {
// Complex sorting logic
})
}, [data, sortKey, sortDirection])
// ✅ Good - Object reference stability
const contextValue = useMemo(() => ({
user,
permissions,
updateUser
}), [user, permissions, updateUser])
```
#### Filtering and Sorting Operations
Always memoize filter and sort operations, especially on large datasets:
```tsx
// ✅ Good - Memoized filtering
export function ProductList({ products, searchTerm, filters }) {
const filteredProducts = useMemo(() => {
return products
.filter(product => {
// Check search term
if (searchTerm && !product.name.toLowerCase().includes(searchTerm.toLowerCase())) {
return false
}
// Check price range
if (filters.minPrice && product.price < filters.minPrice) {
return false
}
if (filters.maxPrice && product.price > filters.maxPrice) {
return false
}
// Check category
if (filters.category && product.category !== filters.category) {
return false
}
return true
})
}, [products, searchTerm, filters])
// ✅ Good - Memoized sorting
const sortedProducts = useMemo(() => {
const sorted = [...filteredProducts]
switch (filters.sortBy) {
case 'price-asc':
return sorted.sort((a, b) => a.price - b.price)
case 'price-desc':
return sorted.sort((a, b) => b.price - a.price)
case 'name':
return sorted.sort((a, b) => a.name.localeCompare(b.name))
case 'rating':
return sorted.sort((a, b) => b.rating - a.rating)
default:
return sorted
}
}, [filteredProducts, filters.sortBy])
return (
<div>
{sortedProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
// ❌ Bad - Recalculates on every render
export function BadProductList({ products, searchTerm }) {
// This runs on EVERY render, even if products and searchTerm haven't changed
const filtered = products.filter(p =>
p.name.includes(searchTerm)
)
return <>{/* render */}</>
}
```
#### Memoizing Context Values
Always memoize context values to prevent all consumers from re-rendering:
```tsx
// ✅ Good - Memoized context value
export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [permissions, setPermissions] = useState<string[]>([])
const login = useCallback(async (credentials: Credentials) => {
setLoading(true)
try {
const response = await authService.login(credentials)
setUser(response.user)
setPermissions(response.permissions)
} finally {
setLoading(false)
}
}, [])
const logout = useCallback(async () => {
await authService.logout()
setUser(null)
setPermissions([])
}, [])
// ✅ Memoize the entire context value
const contextValue = useMemo(() => ({
user,
loading,
permissions,
login,
logout,
isAuthenticated: !!user,
hasPermission: (permission: string) => permissions.includes(permission)
}), [user, loading, permissions, login, logout])
return (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
)
}
// ❌ Bad - Creates new object on every render
export function BadAuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null)
// This creates a new object reference every time BadAuthProvider renders
// causing ALL consumers to re-render
return (
<AuthContext.Provider value={{
user,
setUser,
isAuthenticated: !!user
}}>
{children}
</AuthContext.Provider>
)
}
```
#### Expensive Computed Values
Memoize any computation that involves loops, complex calculations, or data transformations:
```tsx
// ✅ Good - Complex calculations memoized
export function Dashboard({ transactions, exchangeRates }) {
// Calculate totals by currency
const totalsByCurrency = useMemo(() => {
const totals = new Map<string, number>()
transactions.forEach(transaction => {
const current = totals.get(transaction.currency) || 0
totals.set(transaction.currency, current + transaction.amount)
})
return totals
}, [transactions])
// Convert all to base currency
const totalInBaseCurrency = useMemo(() => {
let total = 0
totalsByCurrency.forEach((amount, currency) => {
const rate = exchangeRates[currency] || 1
total += amount * rate
})
return total
}, [totalsByCurrency, exchangeRates])
// Calculate statistics
const statistics = useMemo(() => {
const amounts = transactions.map(t => t.amount)
const sorted = [...amounts].sort((a, b) => a - b)
return {
count: transactions.length,
sum: amounts.reduce((a, b) => a + b, 0),
average: amounts.reduce((a, b) => a + b, 0) / amounts.length,
median: sorted[Math.floor(sorted.length / 2)],
min: Math.min(...amounts),
max: Math.max(...amounts)
}
}, [transactions])
return (
<div>
<TotalDisplay total={totalInBaseCurrency} />
<StatisticsPanel stats={statistics} />
<CurrencyBreakdown totals={totalsByCurrency} />
</div>
)
}
// ✅ Good - Derived state memoized
export function SearchableTable({ data, columns }) {
const [searchTerm, setSearchTerm] = useState('')
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' })
// Memoize search results
const searchResults = useMemo(() => {
if (!searchTerm) return data
return data.filter(row =>
columns.some(col =>
String(row[col.key])
.toLowerCase()
.includes(searchTerm.toLowerCase())
)
)
}, [data, columns, searchTerm])
// Memoize sorted results
const sortedResults = useMemo(() => {
if (!sortConfig.key) return searchResults
return [...searchResults].sort((a, b) => {
const aVal = a[sortConfig.key]
const bVal = b[sortConfig.key]
if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1
if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1
return 0
})
}, [searchResults, sortConfig])
return (
<div>
<SearchInput value={searchTerm} onChange={setSearchTerm} />
<Table data={sortedResults} onSort={setSortConfig} />
</div>
)
}
```
#### When NOT to Use useMemo
Avoid over-optimization with useMemo:
```tsx
// ❌ Bad - Simple calculations don't need memoization
const doubled = useMemo(() => number * 2, [number])
// ✅ Good - Just calculate directly
const doubled = number * 2
// ❌ Bad - Creating new objects/arrays that are passed down immediately
const style = useMemo(() => ({ color: 'red' }), [])
// ✅ Good - Define outside component or use constant
const STYLE = { color: 'red' }
// ❌ Bad - Memoizing primitives
const isEven = useMemo(() => number % 2 === 0, [number])
// ✅ Good - Direct calculation
const isEven = number % 2 === 0
```
### React.memo Usage
Apply `React.memo` to:
- Pure presentational components
- Components that re-render frequently
- Components with expensive render logic
```tsx
// ✅ Good - Memoized list item
export const ListItem = React.memo<ListItemProps>(({ item, onSelect }) => {
return (
<Card onClick={() => onSelect(item.id)}>
<CardContent>{item.name}</CardContent>
</Card>
)
})
// ✅ Good - Custom comparison
export const DataGrid = React.memo<DataGridProps>(
({ data, columns }) => {
// Complex rendering logic
},
(prevProps, nextProps) => {
// Custom comparison logic
return prevProps.data.length === nextProps.data.length &&
prevProps.columns.length === nextProps.columns.length
}
)
```
### Code Splitting
Use dynamic imports for:
- Route-based splitting
- Heavy components (charts, editors)
- Modal content
```tsx
// ✅ Good - Route splitting
const FlightEditor = dynamic(
() => import('@/features/Flights/FlightEditor'),
{
loading: () => <ComponentLoader />,
ssr: false
}
)
// ✅ Good - Modal splitting
const [showModal, setShowModal] = useState(false)
const ConfigModal = useMemo(() => {
if (!showModal) return null
return dynamic(
() => import('./ConfigModal'),
{ loading: () => <ComponentLoader dialog /> }
)
}, [showModal])
```
## Common Component Abstractions
### List Components
**Pattern:** Generic list with flexible rendering
```tsx
interface ListProps<T> {
readonly items: ReadonlyArray<T>
readonly renderItem: (item: T, index: number) => ReactNode
readonly keyExtractor: (item: T, index: number) => string
readonly emptyState?: ReactNode
readonly loading?: boolean
}
export function List<T>({ items, renderItem, keyExtractor, emptyState, loading }: ListProps<T>) {
if (loading) return <ListSkeleton />
if (items.length === 0) return <>{emptyState || <EmptyState />}</>
return (
<Box>
{items.map((item, index) => (
<Box key={keyExtractor(item, index)}>
{renderItem(item, index)}
</Box>
))}
</Box>
)
}
```
### Form Components
**Pattern:** Controlled inputs with validation
```tsx
interface FormFieldProps {
readonly name: string
readonly value: string
readonly error?: string
readonly required?: boolean
readonly disabled?: boolean
readonly onChange: (name: string, value: string) => void
readonly onBlur?: (name: string) => void
}
export function FormField({ name, value, error, onChange, onBlur, ...props }: FormFieldProps) {
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
onChange(name, e.target.value)
}, [name, onChange])
const handleBlur = useCallback(() => {
onBlur?.(name)
}, [name, onBlur])
return (
<TextField
name={name}
value={value}
error={!!error}
helperText={error}
onChange={handleChange}
onBlur={handleBlur}
{...props}
/>
)
}
```
### Modal Components
**Pattern:** Parent-controlled modals with loading states
```tsx
interface ModalProps {
readonly open: boolean
readonly onClose: () => void
readonly title?: string
readonly actions?: ReactNode
readonly children: ReactNode
}
export function Modal({ open, onClose, title, actions, children }: ModalProps) {
return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
{title && <DialogTitle>{title}</DialogTitle>}
<DialogContent>{children}</DialogContent>
{actions && <DialogActions>{actions}</DialogActions>}
</Dialog>
)
}
// Usage with dynamic import
const EditModal = dynamic(
() => import('./EditModal'),
{ loading: () => <ComponentLoader dialog /> }
)
```
### Error Boundary Pattern
Wrap features with error boundaries:
```tsx
interface ErrorBoundaryProps {
readonly children: ReactNode
readonly fallback?: ComponentType<{ error: Error }>
}
class ErrorBoundary extends Component<ErrorBoundaryProps, { error: Error | null }> {
state = { error: null }
static getDerivedStateFromError(error: Error) {
return { error }
}
render() {
if (this.state.error) {
const Fallback = this.props.fallback || DefaultErrorFallback
return <Fallback error={this.state.error} />
}
return this.props.children
}
}
// Wrap features
<ErrorBoundary>
<FeatureComponent />
</ErrorBoundary>
```
## Server vs Client Components Decision Tree
### Use Server Components When:
- Fetching data from APIs
- Accessing backend resources directly
- Using server-only packages
- Rendering static content
- No interactivity needed
### Use Client Components When:
- Using state or effects
- Handling user interactions
- Using browser APIs
- Using third-party client libraries
- Subscribing to data changes
### Migration Pattern:
```tsx
// Server Component (default)
async function ProductList() {
const products = await getProducts() // Direct DB access
return <ProductGrid products={products} />
}
// Client Component (add directive)
'use client'
function ProductGrid({ products }) {
const [filter, setFilter] = useState('')
// Interactive filtering logic
}
```
## Summary
This guide represents our established patterns for building consistent, maintainable components. Key takeaways:
1. **Always use TypeScript** with readonly props
2. **Memoize callbacks** passed as props
3. **Use context** when prop drilling exceeds 2-3 levels
4. **Clean props** before spreading to DOM elements
5. **Prefer composition** over complex component hierarchies
6. **Optimize strategically** with React.memo and useMemo
7. **Split code** at route and modal boundaries
Follow these patterns to maintain consistency across the codebase and ensure optimal performance.
<validation-schema for="react-component">
<check id="file-extension">
<pattern>\.tsx$</pattern>
<message>React components must use .tsx extension</message>
</check>
<check id="component-definition">
<pattern>export const \w+: React\.FC</pattern>
<message>Components must be arrow functions with React.FC type</message>
</check>
<check id="props-interface">
<pattern>readonly \w+[?:]</pattern>
<message>All props must be marked readonly</message>
</check>
<check id="callback-memoization">
<pattern>useCallback\([^)]+\[[^\]]*\]\)</pattern>
<message>Callbacks passed as props must be memoized</message>
</check>
<check id="no-class-components">
<forbidden>class .+ extends (React\.)?Component</forbidden>
<message>Use functional components only, no class components</message>
</check>
</validation-schema>
<code-template id="component-basic">
<description>Basic functional component with TypeScript</description>
<variables>
<var name="COMPONENT_NAME" type="string" example="UserCard" />
<var name="PROP_NAME" type="string" example="user" />
<var name="PROP_TYPE" type="string" example="User" />
</variables>
<template>
import React from 'react';
import { Box, Typography } from '@mui/material';
export type ${COMPONENT_NAME}Props = {
readonly ${PROP_NAME}: ${PROP_TYPE};
readonly onClick?: (id: string) => void;
};
export const ${COMPONENT_NAME}: React.FC<${COMPONENT_NAME}Props> = ({ ${PROP_NAME}, onClick }) => {
const handleClick = useCallback(() => {
if (onClick) {
onClick(${PROP_NAME}.id);
}
}, [onClick, ${PROP_NAME}.id]);
return (
<Box onClick={handleClick} sx={{ cursor: onClick ? 'pointer' : 'default' }}>
<Typography>{${PROP_NAME}.name}</Typography>
</Box>
);
};
</template>
</code-template>