# Component Patterns Modern React component architecture for the application emphasizing type safety, lazy loading, and Suspense boundaries. --- ## React.FC Pattern (PREFERRED) ### Why React.FC All components use the `React.FC` pattern for: - Explicit type safety for props - Consistent component signatures - Clear prop interface documentation - Better IDE autocomplete ### Basic Pattern ```typescript import React from 'react'; interface MyComponentProps { /** User ID to display */ userId: number; /** Optional callback when action occurs */ onAction?: () => void; } export const MyComponent: React.FC = ({ userId, onAction }) => { return (
User: {userId}
); }; export default MyComponent; ``` **Key Points:** - Props interface defined separately with JSDoc comments - `React.FC` provides type safety - Destructure props in parameters - Default export at bottom --- ## Lazy Loading Pattern ### When to Lazy Load Lazy load components that are: - Heavy (DataGrid, charts, rich text editors) - Route-level components - Modal/dialog content (not shown initially) - Below-the-fold content ### How to Lazy Load ```typescript import React from 'react'; // Lazy load heavy component const PostDataGrid = React.lazy(() => import('./grids/PostDataGrid') ); // For named exports const MyComponent = React.lazy(() => import('./MyComponent').then(module => ({ default: module.MyComponent })) ); ``` **Example from PostTable.tsx:** ```typescript /** * Main post table container component */ import React, { useState, useCallback } from 'react'; import { Box, Paper } from '@mui/material'; // Lazy load PostDataGrid to optimize bundle size const PostDataGrid = React.lazy(() => import('./grids/PostDataGrid')); import { SuspenseLoader } from '~components/SuspenseLoader'; export const PostTable: React.FC = ({ formId }) => { return ( ); }; export default PostTable; ``` --- ## Suspense Boundaries ### SuspenseLoader Component **Import:** ```typescript import { SuspenseLoader } from '~components/SuspenseLoader'; // Or import { SuspenseLoader } from '@/components/SuspenseLoader'; ``` **Usage:** ```typescript ``` **What it does:** - Shows loading indicator while lazy component loads - Smooth fade-in animation - Consistent loading experience - Prevents layout shift ### Where to Place Suspense Boundaries **Route Level:** ```typescript // routes/my-route/index.tsx const MyPage = lazy(() => import('@/features/my-feature/components/MyPage')); function Route() { return ( ); } ``` **Component Level:** ```typescript function ParentComponent() { return (
); } ``` **Multiple Boundaries:** ```typescript function Page() { return ( ); } ``` Each section loads independently, better UX. --- ## Component Structure Template ### Recommended Order ```typescript /** * Component description * What it does, when to use it */ import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { Box, Paper, Button } from '@mui/material'; import type { SxProps, Theme } from '@mui/material'; import { useSuspenseQuery } from '@tanstack/react-query'; // Feature imports import { myFeatureApi } from '../api/myFeatureApi'; import type { MyData } from '~types/myData'; // Component imports import { SuspenseLoader } from '~components/SuspenseLoader'; // Hooks import { useAuth } from '@/hooks/useAuth'; import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; // 1. PROPS INTERFACE (with JSDoc) interface MyComponentProps { /** The ID of the entity to display */ entityId: number; /** Optional callback when action completes */ onComplete?: () => void; /** Display mode */ mode?: 'view' | 'edit'; } // 2. STYLES (if inline and <100 lines) const componentStyles: Record> = { container: { p: 2, display: 'flex', flexDirection: 'column', }, header: { mb: 2, display: 'flex', justifyContent: 'space-between', }, }; // 3. COMPONENT DEFINITION export const MyComponent: React.FC = ({ entityId, onComplete, mode = 'view', }) => { // 4. HOOKS (in this order) // - Context hooks first const { user } = useAuth(); const { showSuccess, showError } = useMuiSnackbar(); // - Data fetching const { data } = useSuspenseQuery({ queryKey: ['myEntity', entityId], queryFn: () => myFeatureApi.getEntity(entityId), }); // - Local state const [selectedItem, setSelectedItem] = useState(null); const [isEditing, setIsEditing] = useState(mode === 'edit'); // - Memoized values const filteredData = useMemo(() => { return data.filter(item => item.active); }, [data]); // - Effects useEffect(() => { // Setup return () => { // Cleanup }; }, []); // 5. EVENT HANDLERS (with useCallback) const handleItemSelect = useCallback((itemId: string) => { setSelectedItem(itemId); }, []); const handleSave = useCallback(async () => { try { await myFeatureApi.updateEntity(entityId, { /* data */ }); showSuccess('Entity updated successfully'); onComplete?.(); } catch (error) { showError('Failed to update entity'); } }, [entityId, onComplete, showSuccess, showError]); // 6. RENDER return (

My Component

{filteredData.map(item => (
{item.name}
))}
); }; // 7. EXPORT (default export at bottom) export default MyComponent; ``` --- ## Component Separation ### When to Split Components **Split into multiple components when:** - Component exceeds 300 lines - Multiple distinct responsibilities - Reusable sections - Complex nested JSX **Example:** ```typescript // ❌ AVOID - Monolithic function MassiveComponent() { // 500+ lines // Search logic // Filter logic // Grid logic // Action panel logic } // ✅ PREFERRED - Modular function ParentContainer() { return ( ); } ``` ### When to Keep Together **Keep in same file when:** - Component < 200 lines - Tightly coupled logic - Not reusable elsewhere - Simple presentation component --- ## Export Patterns ### Named Const + Default Export (PREFERRED) ```typescript export const MyComponent: React.FC = ({ ... }) => { // Component logic }; export default MyComponent; ``` **Why:** - Named export for testing/refactoring - Default export for lazy loading convenience - Both options available to consumers ### Lazy Loading Named Exports ```typescript const MyComponent = React.lazy(() => import('./MyComponent').then(module => ({ default: module.MyComponent })) ); ``` --- ## Component Communication ### Props Down, Events Up ```typescript // Parent function Parent() { const [selectedId, setSelectedId] = useState(null); return ( ); } // Child interface ChildProps { data: Data[]; onSelect: (id: string) => void; } export const Child: React.FC = ({ data, onSelect }) => { return (
onSelect(data[0].id)}> {/* Content */}
); }; ``` ### Avoid Prop Drilling **Use context for deep nesting:** ```typescript // ❌ AVOID - Prop drilling 5+ levels // Finally uses it here // ✅ PREFERRED - Context or TanStack Query const MyContext = createContext(null); function Provider({ children }) { const { data } = useSuspenseQuery({ ... }); return {children}; } function DeepChild() { const data = useContext(MyContext); // Use data directly } ``` --- ## Advanced Patterns ### Compound Components ```typescript // Card.tsx export const Card: React.FC & { Header: typeof CardHeader; Body: typeof CardBody; Footer: typeof CardFooter; } = ({ children }) => { return {children}; }; Card.Header = CardHeader; Card.Body = CardBody; Card.Footer = CardFooter; // Usage Title Content Actions ``` ### Render Props (Rare, but useful) ```typescript interface DataProviderProps { children: (data: Data) => React.ReactNode; } export const DataProvider: React.FC = ({ children }) => { const { data } = useSuspenseQuery({ ... }); return <>{children(data)}; }; // Usage {(data) => } ``` --- ## Summary **Modern Component Recipe:** 1. `React.FC` with TypeScript 2. Lazy load if heavy: `React.lazy(() => import())` 3. Wrap in `` for loading 4. Use `useSuspenseQuery` for data 5. Import aliases (@/, ~types, ~components) 6. Event handlers with `useCallback` 7. Default export at bottom 8. No early returns for loading states **See Also:** - [data-fetching.md](data-fetching.md) - useSuspenseQuery details - [loading-and-error-states.md](loading-and-error-states.md) - Suspense best practices - [complete-examples.md](complete-examples.md) - Full working examples