All skills
Skillintermediate

Component Patterns

Modern React component architecture for the application emphasizing type safety, lazy loading, and Suspense boundaries.

Claude Code Knowledge Pack7/10/2026

Overview

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


interface MyComponentProps {
    /** User ID to display */
    userId: number;
    /** Optional callback when action occurs */
    onAction?: () => void;
}

    return (
        <div>
            User: {userId}
        </div>
    );
};

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


// 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:

/**
 * Main post table container component
 */

// Lazy load PostDataGrid to optimize bundle size
const PostDataGrid = React.lazy(() => import('./grids/PostDataGrid'));

    return (
        
            
                
            
        
    );
};


Suspense Boundaries

SuspenseLoader Component

Import:


// Or

Usage:


    

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:

// routes/my-route/index.tsx
const MyPage = lazy(() => import('@/features/my-feature/components/MyPage'));

function Route() {
    return (
        
            
        
    );
}

Component Level:

function ParentComponent() {
    return (
        
            
            
                
            
        
    );
}

Multiple Boundaries:

function Page() {
    return (
        
            
                
            

            
                
            

            
                
            
        
    );
}

Each section loads independently, better UX.


Component Structure Template

Recommended Order

/**
 * Component description
 * What it does, when to use it
 */

// Feature imports

// Component imports

// Hooks

// 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<string, SxProps> = {
    container: {
        p: 2,
        display: 'flex',
        flexDirection: 'column',
    },
    header: {
        mb: 2,
        display: 'flex',
        justifyContent: 'space-between',
    },
};

// 3. COMPONENT DEFINITION

    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<string | null>(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 (
        
            
                <h2>My Component</h2>
                Save
            

            
                {filteredData.map(item => (
                    <div key={item.id}>{item.name}</div>
                ))}
            
        
    );
};

// 7. EXPORT (default export at bottom)


Component Separation

When to Split Components

Split into multiple components when:

  • Component exceeds 300 lines
  • Multiple distinct responsibilities
  • Reusable sections
  • Complex nested JSX

Example:

// ❌ 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)


    // Component logic
};

Why:

  • Named export for testing/refactoring
  • Default export for lazy loading convenience
  • Both options available to consumers

Lazy Loading Named Exports

const MyComponent = React.lazy(() =>
    import('./MyComponent').then(module => ({
        default: module.MyComponent
    }))
);

Component Communication

Props Down, Events Up

// Parent
function Parent() {
    const [selectedId, setSelectedId] = useState<string | null>(null);

    return (
        
    );
}

// Child
interface ChildProps {
    data: Data[];
    onSelect: (id: string) => void;
}

    return (
        <div onClick={() => onSelect(data[0].id)}>
            
        </div>
    );
};

Avoid Prop Drilling

Use context for deep nesting:

// ❌ 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 <MyContext.Provider value={data}>{children}</MyContext.Provider>;
}

function DeepChild() {
    const data = useContext(MyContext);
    // Use data directly
}

Advanced Patterns

Compound Components

// Card.tsx

    Header: typeof CardHeader;
    Body: typeof CardBody;
    Footer: typeof CardFooter;
} = ({ children }) => {
    return {children};
};

Card.Header = CardHeader;
Card.Body = CardBody;
Card.Footer = CardFooter;

// Usage

    <Card.Header>Title</Card.Header>
    <Card.Body>Content</Card.Body>
    <Card.Footer>Actions</Card.Footer>

Render Props (Rare, but useful)

interface DataProviderProps {
    children: (data: Data) => React.ReactNode;
}

    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: