Files
antigravity-skills-reference/web-app/public/skills/frontend-dev-guidelines/resources/component-patterns.md

502 lines
11 KiB
Markdown

# 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<Props>` 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<MyComponentProps> = ({ userId, onAction }) => {
return (
<div>
User: {userId}
</div>
);
};
export default MyComponent;
```
**Key Points:**
- Props interface defined separately with JSDoc comments
- `React.FC<Props>` 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<PostTableProps> = ({ formId }) => {
return (
<Box>
<SuspenseLoader>
<PostDataGrid formId={formId} />
</SuspenseLoader>
</Box>
);
};
export default PostTable;
```
---
## Suspense Boundaries
### SuspenseLoader Component
**Import:**
```typescript
import { SuspenseLoader } from '~components/SuspenseLoader';
// Or
import { SuspenseLoader } from '@/components/SuspenseLoader';
```
**Usage:**
```typescript
<SuspenseLoader>
<LazyLoadedComponent />
</SuspenseLoader>
```
**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 (
<SuspenseLoader>
<MyPage />
</SuspenseLoader>
);
}
```
**Component Level:**
```typescript
function ParentComponent() {
return (
<Box>
<Header />
<SuspenseLoader>
<HeavyDataGrid />
</SuspenseLoader>
</Box>
);
}
```
**Multiple Boundaries:**
```typescript
function Page() {
return (
<Box>
<SuspenseLoader>
<HeaderSection />
</SuspenseLoader>
<SuspenseLoader>
<MainContent />
</SuspenseLoader>
<SuspenseLoader>
<Sidebar />
</SuspenseLoader>
</Box>
);
}
```
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<string, SxProps<Theme>> = {
container: {
p: 2,
display: 'flex',
flexDirection: 'column',
},
header: {
mb: 2,
display: 'flex',
justifyContent: 'space-between',
},
};
// 3. COMPONENT DEFINITION
export const MyComponent: React.FC<MyComponentProps> = ({
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 (
<Box sx={componentStyles.container}>
<Box sx={componentStyles.header}>
<h2>My Component</h2>
<Button onClick={handleSave}>Save</Button>
</Box>
<Paper sx={{ p: 2 }}>
{filteredData.map(item => (
<div key={item.id}>{item.name}</div>
))}
</Paper>
</Box>
);
};
// 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 (
<Box>
<SearchAndFilter onFilter={handleFilter} />
<DataGrid data={filteredData} />
<ActionPanel onAction={handleAction} />
</Box>
);
}
```
### 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<Props> = ({ ... }) => {
// 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<string | null>(null);
return (
<Child
data={data} // Props down
onSelect={setSelectedId} // Events up
/>
);
}
// Child
interface ChildProps {
data: Data[];
onSelect: (id: string) => void;
}
export const Child: React.FC<ChildProps> = ({ data, onSelect }) => {
return (
<div onClick={() => onSelect(data[0].id)}>
{/* Content */}
</div>
);
};
```
### Avoid Prop Drilling
**Use context for deep nesting:**
```typescript
// ❌ AVOID - Prop drilling 5+ levels
<A prop={x}>
<B prop={x}>
<C prop={x}>
<D prop={x}>
<E prop={x} /> // Finally uses it here
</D>
</C>
</B>
</A>
// ✅ PREFERRED - Context or TanStack Query
const MyContext = createContext<MyData | null>(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
```typescript
// Card.tsx
export const Card: React.FC<CardProps> & {
Header: typeof CardHeader;
Body: typeof CardBody;
Footer: typeof CardFooter;
} = ({ children }) => {
return <Paper>{children}</Paper>;
};
Card.Header = CardHeader;
Card.Body = CardBody;
Card.Footer = CardFooter;
// Usage
<Card>
<Card.Header>Title</Card.Header>
<Card.Body>Content</Card.Body>
<Card.Footer>Actions</Card.Footer>
</Card>
```
### Render Props (Rare, but useful)
```typescript
interface DataProviderProps {
children: (data: Data) => React.ReactNode;
}
export const DataProvider: React.FC<DataProviderProps> = ({ children }) => {
const { data } = useSuspenseQuery({ ... });
return <>{children(data)}</>;
};
// Usage
<DataProvider>
{(data) => <Display data={data} />}
</DataProvider>
```
---
## Summary
**Modern Component Recipe:**
1. `React.FC<Props>` with TypeScript
2. Lazy load if heavy: `React.lazy(() => import())`
3. Wrap in `<SuspenseLoader>` 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