UNPKG

@jjdenhertog/ai-driven-development

Version:

AI-driven development workflow with learning capabilities for Claude

1,192 lines (984 loc) 33.1 kB
--- 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&lt;Props&gt; = ({ prop }) =&gt; {}</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>