Replace placeholder content with real frontend development guidance: References: - react_patterns.md: Compound Components, Render Props, Custom Hooks - nextjs_optimization_guide.md: Server/Client Components, ISR, caching - frontend_best_practices.md: Accessibility, testing, TypeScript patterns Scripts: - frontend_scaffolder.py: Generate Next.js/React projects with features - component_generator.py: Generate React components with tests/stories - bundle_analyzer.py: Analyze package.json for optimization opportunities SKILL.md: - Added table of contents - Numbered workflow steps - Removed marketing language - Added trigger phrases in description Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,209 +1,473 @@
|
||||
---
|
||||
name: senior-frontend
|
||||
description: Comprehensive frontend development skill for building modern, performant web applications using ReactJS, NextJS, TypeScript, Tailwind CSS. Includes component scaffolding, performance optimization, bundle analysis, and UI best practices. Use when developing frontend features, optimizing performance, implementing UI/UX designs, managing state, or reviewing frontend code.
|
||||
description: Frontend development skill for React, Next.js, TypeScript, and Tailwind CSS applications. Use when building React components, optimizing Next.js performance, analyzing bundle sizes, scaffolding frontend projects, implementing accessibility, or reviewing frontend code quality.
|
||||
---
|
||||
|
||||
# Senior Frontend
|
||||
|
||||
Complete toolkit for senior frontend with modern tools and best practices.
|
||||
Frontend development patterns, performance optimization, and automation tools for React/Next.js applications.
|
||||
|
||||
## Quick Start
|
||||
## Table of Contents
|
||||
|
||||
### Main Capabilities
|
||||
- [Project Scaffolding](#project-scaffolding)
|
||||
- [Component Generation](#component-generation)
|
||||
- [Bundle Analysis](#bundle-analysis)
|
||||
- [React Patterns](#react-patterns)
|
||||
- [Next.js Optimization](#nextjs-optimization)
|
||||
- [Accessibility and Testing](#accessibility-and-testing)
|
||||
|
||||
This skill provides three core capabilities through automated scripts:
|
||||
---
|
||||
|
||||
```bash
|
||||
# Script 1: Component Generator
|
||||
python scripts/component_generator.py [options]
|
||||
## Project Scaffolding
|
||||
|
||||
# Script 2: Bundle Analyzer
|
||||
python scripts/bundle_analyzer.py [options]
|
||||
Generate a new Next.js or React project with TypeScript, Tailwind CSS, and best practice configurations.
|
||||
|
||||
# Script 3: Frontend Scaffolder
|
||||
python scripts/frontend_scaffolder.py [options]
|
||||
### Workflow: Create New Frontend Project
|
||||
|
||||
1. Run the scaffolder with your project name and template:
|
||||
```bash
|
||||
python scripts/frontend_scaffolder.py my-app --template nextjs
|
||||
```
|
||||
|
||||
2. Add optional features (auth, api, forms, testing, storybook):
|
||||
```bash
|
||||
python scripts/frontend_scaffolder.py dashboard --template nextjs --features auth,api
|
||||
```
|
||||
|
||||
3. Navigate to the project and install dependencies:
|
||||
```bash
|
||||
cd my-app && npm install
|
||||
```
|
||||
|
||||
4. Start the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Scaffolder Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--template nextjs` | Next.js 14+ with App Router and Server Components |
|
||||
| `--template react` | React + Vite with TypeScript |
|
||||
| `--features auth` | Add NextAuth.js authentication |
|
||||
| `--features api` | Add React Query + API client |
|
||||
| `--features forms` | Add React Hook Form + Zod validation |
|
||||
| `--features testing` | Add Vitest + Testing Library |
|
||||
| `--dry-run` | Preview files without creating them |
|
||||
|
||||
### Generated Structure (Next.js)
|
||||
|
||||
```
|
||||
my-app/
|
||||
├── app/
|
||||
│ ├── layout.tsx # Root layout with fonts
|
||||
│ ├── page.tsx # Home page
|
||||
│ ├── globals.css # Tailwind + CSS variables
|
||||
│ └── api/health/route.ts
|
||||
├── components/
|
||||
│ ├── ui/ # Button, Input, Card
|
||||
│ └── layout/ # Header, Footer, Sidebar
|
||||
├── hooks/ # useDebounce, useLocalStorage
|
||||
├── lib/ # utils (cn), constants
|
||||
├── types/ # TypeScript interfaces
|
||||
├── tailwind.config.ts
|
||||
├── next.config.js
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Core Capabilities
|
||||
---
|
||||
|
||||
### 1. Component Generator
|
||||
## Component Generation
|
||||
|
||||
Automated tool for component generator tasks.
|
||||
Generate React components with TypeScript, tests, and Storybook stories.
|
||||
|
||||
**Features:**
|
||||
- Automated scaffolding
|
||||
- Best practices built-in
|
||||
- Configurable templates
|
||||
- Quality checks
|
||||
### Workflow: Create a New Component
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python scripts/component_generator.py <project-path> [options]
|
||||
1. Generate a client component:
|
||||
```bash
|
||||
python scripts/component_generator.py Button --dir src/components/ui
|
||||
```
|
||||
|
||||
2. Generate a server component:
|
||||
```bash
|
||||
python scripts/component_generator.py ProductCard --type server
|
||||
```
|
||||
|
||||
3. Generate with test and story files:
|
||||
```bash
|
||||
python scripts/component_generator.py UserProfile --with-test --with-story
|
||||
```
|
||||
|
||||
4. Generate a custom hook:
|
||||
```bash
|
||||
python scripts/component_generator.py FormValidation --type hook
|
||||
```
|
||||
|
||||
### Generator Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--type client` | Client component with 'use client' (default) |
|
||||
| `--type server` | Async server component |
|
||||
| `--type hook` | Custom React hook |
|
||||
| `--with-test` | Include test file |
|
||||
| `--with-story` | Include Storybook story |
|
||||
| `--flat` | Create in output dir without subdirectory |
|
||||
| `--dry-run` | Preview without creating files |
|
||||
|
||||
### Generated Component Example
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ButtonProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Button({ className, children }: ButtonProps) {
|
||||
return (
|
||||
<div className={cn('', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Bundle Analyzer
|
||||
---
|
||||
|
||||
Comprehensive analysis and optimization tool.
|
||||
## Bundle Analysis
|
||||
|
||||
**Features:**
|
||||
- Deep analysis
|
||||
- Performance metrics
|
||||
- Recommendations
|
||||
- Automated fixes
|
||||
Analyze package.json and project structure for bundle optimization opportunities.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python scripts/bundle_analyzer.py <target-path> [--verbose]
|
||||
### Workflow: Optimize Bundle Size
|
||||
|
||||
1. Run the analyzer on your project:
|
||||
```bash
|
||||
python scripts/bundle_analyzer.py /path/to/project
|
||||
```
|
||||
|
||||
2. Review the health score and issues:
|
||||
```
|
||||
Bundle Health Score: 75/100 (C)
|
||||
|
||||
HEAVY DEPENDENCIES:
|
||||
moment (290KB)
|
||||
Alternative: date-fns (12KB) or dayjs (2KB)
|
||||
|
||||
lodash (71KB)
|
||||
Alternative: lodash-es with tree-shaking
|
||||
```
|
||||
|
||||
3. Apply the recommended fixes by replacing heavy dependencies.
|
||||
|
||||
4. Re-run with verbose mode to check import patterns:
|
||||
```bash
|
||||
python scripts/bundle_analyzer.py . --verbose
|
||||
```
|
||||
|
||||
### Bundle Score Interpretation
|
||||
|
||||
| Score | Grade | Action |
|
||||
|-------|-------|--------|
|
||||
| 90-100 | A | Bundle is well-optimized |
|
||||
| 80-89 | B | Minor optimizations available |
|
||||
| 70-79 | C | Replace heavy dependencies |
|
||||
| 60-69 | D | Multiple issues need attention |
|
||||
| 0-59 | F | Critical bundle size problems |
|
||||
|
||||
### Heavy Dependencies Detected
|
||||
|
||||
The analyzer identifies these common heavy packages:
|
||||
|
||||
| Package | Size | Alternative |
|
||||
|---------|------|-------------|
|
||||
| moment | 290KB | date-fns (12KB) or dayjs (2KB) |
|
||||
| lodash | 71KB | lodash-es with tree-shaking |
|
||||
| axios | 14KB | Native fetch or ky (3KB) |
|
||||
| jquery | 87KB | Native DOM APIs |
|
||||
| @mui/material | Large | shadcn/ui or Radix UI |
|
||||
|
||||
---
|
||||
|
||||
## React Patterns
|
||||
|
||||
Reference: `references/react_patterns.md`
|
||||
|
||||
### Compound Components
|
||||
|
||||
Share state between related components:
|
||||
|
||||
```tsx
|
||||
const Tabs = ({ children }) => {
|
||||
const [active, setActive] = useState(0);
|
||||
return (
|
||||
<TabsContext.Provider value={{ active, setActive }}>
|
||||
{children}
|
||||
</TabsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
Tabs.List = TabList;
|
||||
Tabs.Panel = TabPanel;
|
||||
|
||||
// Usage
|
||||
<Tabs>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab>One</Tabs.Tab>
|
||||
<Tabs.Tab>Two</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel>Content 1</Tabs.Panel>
|
||||
<Tabs.Panel>Content 2</Tabs.Panel>
|
||||
</Tabs>
|
||||
```
|
||||
|
||||
### 3. Frontend Scaffolder
|
||||
### Custom Hooks
|
||||
|
||||
Advanced tooling for specialized tasks.
|
||||
Extract reusable logic:
|
||||
|
||||
**Features:**
|
||||
- Expert-level automation
|
||||
- Custom configurations
|
||||
- Integration ready
|
||||
- Production-grade output
|
||||
```tsx
|
||||
function useDebounce<T>(value: T, delay = 500): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python scripts/frontend_scaffolder.py [arguments] [options]
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
||||
return () => clearTimeout(timer);
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const debouncedSearch = useDebounce(searchTerm, 300);
|
||||
```
|
||||
|
||||
## Reference Documentation
|
||||
### Render Props
|
||||
|
||||
### React Patterns
|
||||
Share rendering logic:
|
||||
|
||||
Comprehensive guide available in `references/react_patterns.md`:
|
||||
```tsx
|
||||
function DataFetcher({ url, render }) {
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
- Detailed patterns and practices
|
||||
- Code examples
|
||||
- Best practices
|
||||
- Anti-patterns to avoid
|
||||
- Real-world scenarios
|
||||
useEffect(() => {
|
||||
fetch(url).then(r => r.json()).then(setData).finally(() => setLoading(false));
|
||||
}, [url]);
|
||||
|
||||
### Nextjs Optimization Guide
|
||||
return render({ data, loading });
|
||||
}
|
||||
|
||||
Complete workflow documentation in `references/nextjs_optimization_guide.md`:
|
||||
|
||||
- Step-by-step processes
|
||||
- Optimization strategies
|
||||
- Tool integrations
|
||||
- Performance tuning
|
||||
- Troubleshooting guide
|
||||
|
||||
### Frontend Best Practices
|
||||
|
||||
Technical reference guide in `references/frontend_best_practices.md`:
|
||||
|
||||
- Technology stack details
|
||||
- Configuration examples
|
||||
- Integration patterns
|
||||
- Security considerations
|
||||
- Scalability guidelines
|
||||
|
||||
## Tech Stack
|
||||
|
||||
**Languages:** TypeScript, JavaScript, Python, Go, Swift, Kotlin
|
||||
**Frontend:** React, Next.js, React Native, Flutter
|
||||
**Backend:** Node.js, Express, GraphQL, REST APIs
|
||||
**Database:** PostgreSQL, Prisma, NeonDB, Supabase
|
||||
**DevOps:** Docker, Kubernetes, Terraform, GitHub Actions, CircleCI
|
||||
**Cloud:** AWS, GCP, Azure
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Setup and Configuration
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
# or
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Configure environment
|
||||
cp .env.example .env
|
||||
// Usage
|
||||
<DataFetcher
|
||||
url="/api/users"
|
||||
render={({ data, loading }) =>
|
||||
loading ? <Spinner /> : <UserList users={data} />
|
||||
}
|
||||
/>
|
||||
```
|
||||
|
||||
### 2. Run Quality Checks
|
||||
---
|
||||
|
||||
```bash
|
||||
# Use the analyzer script
|
||||
python scripts/bundle_analyzer.py .
|
||||
## Next.js Optimization
|
||||
|
||||
# Review recommendations
|
||||
# Apply fixes
|
||||
Reference: `references/nextjs_optimization_guide.md`
|
||||
|
||||
### Server vs Client Components
|
||||
|
||||
Use Server Components by default. Add 'use client' only when you need:
|
||||
- Event handlers (onClick, onChange)
|
||||
- State (useState, useReducer)
|
||||
- Effects (useEffect)
|
||||
- Browser APIs
|
||||
|
||||
```tsx
|
||||
// Server Component (default) - no 'use client'
|
||||
async function ProductPage({ params }) {
|
||||
const product = await getProduct(params.id); // Server-side fetch
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{product.name}</h1>
|
||||
<AddToCartButton productId={product.id} /> {/* Client component */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Client Component
|
||||
'use client';
|
||||
function AddToCartButton({ productId }) {
|
||||
const [adding, setAdding] = useState(false);
|
||||
return <button onClick={() => addToCart(productId)}>Add</button>;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Implement Best Practices
|
||||
### Image Optimization
|
||||
|
||||
Follow the patterns and practices documented in:
|
||||
- `references/react_patterns.md`
|
||||
- `references/nextjs_optimization_guide.md`
|
||||
- `references/frontend_best_practices.md`
|
||||
```tsx
|
||||
import Image from 'next/image';
|
||||
|
||||
## Best Practices Summary
|
||||
// Above the fold - load immediately
|
||||
<Image
|
||||
src="/hero.jpg"
|
||||
alt="Hero"
|
||||
width={1200}
|
||||
height={600}
|
||||
priority
|
||||
/>
|
||||
|
||||
### Code Quality
|
||||
- Follow established patterns
|
||||
- Write comprehensive tests
|
||||
- Document decisions
|
||||
- Review regularly
|
||||
|
||||
### Performance
|
||||
- Measure before optimizing
|
||||
- Use appropriate caching
|
||||
- Optimize critical paths
|
||||
- Monitor in production
|
||||
|
||||
### Security
|
||||
- Validate all inputs
|
||||
- Use parameterized queries
|
||||
- Implement proper authentication
|
||||
- Keep dependencies updated
|
||||
|
||||
### Maintainability
|
||||
- Write clear code
|
||||
- Use consistent naming
|
||||
- Add helpful comments
|
||||
- Keep it simple
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
npm run dev
|
||||
npm run build
|
||||
npm run test
|
||||
npm run lint
|
||||
|
||||
# Analysis
|
||||
python scripts/bundle_analyzer.py .
|
||||
python scripts/frontend_scaffolder.py --analyze
|
||||
|
||||
# Deployment
|
||||
docker build -t app:latest .
|
||||
docker-compose up -d
|
||||
kubectl apply -f k8s/
|
||||
// Responsive image with fill
|
||||
<div className="relative aspect-video">
|
||||
<Image
|
||||
src="/product.jpg"
|
||||
alt="Product"
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
### Data Fetching Patterns
|
||||
|
||||
### Common Issues
|
||||
```tsx
|
||||
// Parallel fetching
|
||||
async function Dashboard() {
|
||||
const [user, stats] = await Promise.all([
|
||||
getUser(),
|
||||
getStats()
|
||||
]);
|
||||
return <div>...</div>;
|
||||
}
|
||||
|
||||
Check the comprehensive troubleshooting section in `references/frontend_best_practices.md`.
|
||||
// Streaming with Suspense
|
||||
async function ProductPage({ params }) {
|
||||
return (
|
||||
<div>
|
||||
<ProductDetails id={params.id} />
|
||||
<Suspense fallback={<ReviewsSkeleton />}>
|
||||
<Reviews productId={params.id} />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Getting Help
|
||||
---
|
||||
|
||||
- Review reference documentation
|
||||
- Check script output messages
|
||||
- Consult tech stack documentation
|
||||
- Review error logs
|
||||
## Accessibility and Testing
|
||||
|
||||
Reference: `references/frontend_best_practices.md`
|
||||
|
||||
### Accessibility Checklist
|
||||
|
||||
1. **Semantic HTML**: Use proper elements (`<button>`, `<nav>`, `<main>`)
|
||||
2. **Keyboard Navigation**: All interactive elements focusable
|
||||
3. **ARIA Labels**: Provide labels for icons and complex widgets
|
||||
4. **Color Contrast**: Minimum 4.5:1 for normal text
|
||||
5. **Focus Indicators**: Visible focus states
|
||||
|
||||
```tsx
|
||||
// Accessible button
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Close dialog"
|
||||
onClick={onClose}
|
||||
className="focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||
>
|
||||
<XIcon aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
// Skip link for keyboard users
|
||||
<a href="#main-content" className="sr-only focus:not-sr-only">
|
||||
Skip to main content
|
||||
</a>
|
||||
```
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
```tsx
|
||||
// Component test with React Testing Library
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
test('button triggers action on click', async () => {
|
||||
const onClick = vi.fn();
|
||||
render(<Button onClick={onClick}>Click me</Button>);
|
||||
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Test accessibility
|
||||
test('dialog is accessible', async () => {
|
||||
render(<Dialog open={true} title="Confirm" />);
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(screen.getByRole('dialog')).toHaveAttribute('aria-labelledby');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Common Next.js Config
|
||||
|
||||
```js
|
||||
// next.config.js
|
||||
const nextConfig = {
|
||||
images: {
|
||||
remotePatterns: [{ hostname: 'cdn.example.com' }],
|
||||
formats: ['image/avif', 'image/webp'],
|
||||
},
|
||||
experimental: {
|
||||
optimizePackageImports: ['lucide-react', '@heroicons/react'],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Tailwind CSS Utilities
|
||||
|
||||
```tsx
|
||||
// Conditional classes with cn()
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
<button className={cn(
|
||||
'px-4 py-2 rounded',
|
||||
variant === 'primary' && 'bg-blue-500 text-white',
|
||||
disabled && 'opacity-50 cursor-not-allowed'
|
||||
)} />
|
||||
```
|
||||
|
||||
### TypeScript Patterns
|
||||
|
||||
```tsx
|
||||
// Props with children
|
||||
interface CardProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// Generic component
|
||||
interface ListProps<T> {
|
||||
items: T[];
|
||||
renderItem: (item: T) => React.ReactNode;
|
||||
}
|
||||
|
||||
function List<T>({ items, renderItem }: ListProps<T>) {
|
||||
return <ul>{items.map(renderItem)}</ul>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- Pattern Reference: `references/react_patterns.md`
|
||||
- Workflow Guide: `references/nextjs_optimization_guide.md`
|
||||
- Technical Guide: `references/frontend_best_practices.md`
|
||||
- Tool Scripts: `scripts/` directory
|
||||
- React Patterns: `references/react_patterns.md`
|
||||
- Next.js Optimization: `references/nextjs_optimization_guide.md`
|
||||
- Best Practices: `references/frontend_best_practices.md`
|
||||
|
||||
@@ -1,103 +1,806 @@
|
||||
# Frontend Best Practices
|
||||
|
||||
## Overview
|
||||
Modern frontend development standards for accessibility, testing, TypeScript, and Tailwind CSS.
|
||||
|
||||
This reference guide provides comprehensive information for senior frontend.
|
||||
---
|
||||
|
||||
## Patterns and Practices
|
||||
## Table of Contents
|
||||
|
||||
### Pattern 1: Best Practice Implementation
|
||||
- [Accessibility (a11y)](#accessibility-a11y)
|
||||
- [Testing Strategies](#testing-strategies)
|
||||
- [TypeScript Patterns](#typescript-patterns)
|
||||
- [Tailwind CSS](#tailwind-css)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Security](#security)
|
||||
|
||||
**Description:**
|
||||
Detailed explanation of the pattern.
|
||||
---
|
||||
|
||||
**When to Use:**
|
||||
- Scenario 1
|
||||
- Scenario 2
|
||||
- Scenario 3
|
||||
## Accessibility (a11y)
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Example code implementation
|
||||
export class Example {
|
||||
// Implementation details
|
||||
### Semantic HTML
|
||||
|
||||
```tsx
|
||||
// BAD - Divs for everything
|
||||
<div onClick={handleClick}>Click me</div>
|
||||
<div class="header">...</div>
|
||||
<div class="nav">...</div>
|
||||
|
||||
// GOOD - Semantic elements
|
||||
<button onClick={handleClick}>Click me</button>
|
||||
<header>...</header>
|
||||
<nav>...</nav>
|
||||
<main>...</main>
|
||||
<article>...</article>
|
||||
<aside>...</aside>
|
||||
<footer>...</footer>
|
||||
```
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
```tsx
|
||||
// Ensure all interactive elements are keyboard accessible
|
||||
function Modal({ isOpen, onClose, children }: ModalProps) {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
// Focus first focusable element
|
||||
const focusable = modalRef.current?.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
(focusable?.[0] as HTMLElement)?.focus();
|
||||
|
||||
// Trap focus within modal
|
||||
const handleTab = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Tab' && focusable) {
|
||||
const first = focusable[0] as HTMLElement;
|
||||
const last = focusable[focusable.length - 1] as HTMLElement;
|
||||
|
||||
if (e.shiftKey && document.activeElement === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleTab);
|
||||
return () => document.removeEventListener('keydown', handleTab);
|
||||
}
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={modalRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-title"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Benefit 1
|
||||
- Benefit 2
|
||||
- Benefit 3
|
||||
### ARIA Attributes
|
||||
|
||||
**Trade-offs:**
|
||||
- Consider 1
|
||||
- Consider 2
|
||||
- Consider 3
|
||||
```tsx
|
||||
// Live regions for dynamic content
|
||||
<div aria-live="polite" aria-atomic="true">
|
||||
{status && <p>{status}</p>}
|
||||
</div>
|
||||
|
||||
### Pattern 2: Advanced Technique
|
||||
// Loading states
|
||||
<button disabled={isLoading} aria-busy={isLoading}>
|
||||
{isLoading ? 'Loading...' : 'Submit'}
|
||||
</button>
|
||||
|
||||
**Description:**
|
||||
Another important pattern for senior frontend.
|
||||
// Form labels
|
||||
<label htmlFor="email">Email address</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
aria-required="true"
|
||||
aria-invalid={!!errors.email}
|
||||
aria-describedby={errors.email ? 'email-error' : undefined}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p id="email-error" role="alert">
|
||||
{errors.email}
|
||||
</p>
|
||||
)}
|
||||
|
||||
// Navigation
|
||||
<nav aria-label="Main navigation">
|
||||
<ul>
|
||||
<li><a href="/" aria-current={isHome ? 'page' : undefined}>Home</a></li>
|
||||
<li><a href="/about" aria-current={isAbout ? 'page' : undefined}>About</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
// Toggle buttons
|
||||
<button
|
||||
aria-pressed={isEnabled}
|
||||
onClick={() => setIsEnabled(!isEnabled)}
|
||||
>
|
||||
{isEnabled ? 'Enabled' : 'Disabled'}
|
||||
</button>
|
||||
|
||||
// Expandable sections
|
||||
<button
|
||||
aria-expanded={isOpen}
|
||||
aria-controls="content-panel"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
Show details
|
||||
</button>
|
||||
<div id="content-panel" hidden={!isOpen}>
|
||||
Content here
|
||||
</div>
|
||||
```
|
||||
|
||||
### Color Contrast
|
||||
|
||||
```tsx
|
||||
// Ensure 4.5:1 contrast ratio for text (WCAG AA)
|
||||
// Use tools like @axe-core/react for testing
|
||||
|
||||
// tailwind.config.js - Define accessible colors
|
||||
module.exports = {
|
||||
theme: {
|
||||
colors: {
|
||||
// Primary with proper contrast
|
||||
primary: {
|
||||
DEFAULT: '#2563eb', // Blue 600
|
||||
foreground: '#ffffff',
|
||||
},
|
||||
// Error state
|
||||
error: {
|
||||
DEFAULT: '#dc2626', // Red 600
|
||||
foreground: '#ffffff',
|
||||
},
|
||||
// Text colors with proper contrast
|
||||
foreground: '#0f172a', // Slate 900
|
||||
muted: '#64748b', // Slate 500 - minimum 4.5:1 on white
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Never rely on color alone
|
||||
<span className="text-red-600">
|
||||
<ErrorIcon aria-hidden="true" />
|
||||
<span>Error: Invalid input</span>
|
||||
</span>
|
||||
```
|
||||
|
||||
### Screen Reader Only Content
|
||||
|
||||
```tsx
|
||||
// Visually hidden but accessible to screen readers
|
||||
const srOnly = 'absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0';
|
||||
|
||||
// Skip link for keyboard users
|
||||
<a href="#main-content" className={srOnly + ' focus:not-sr-only focus:absolute focus:top-0'}>
|
||||
Skip to main content
|
||||
</a>
|
||||
|
||||
// Icon buttons need labels
|
||||
<button aria-label="Close menu">
|
||||
<XIcon aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
// Or use visually hidden text
|
||||
<button>
|
||||
<XIcon aria-hidden="true" />
|
||||
<span className={srOnly}>Close menu</span>
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategies
|
||||
|
||||
### Component Testing with Testing Library
|
||||
|
||||
```tsx
|
||||
// Button.test.tsx
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Button } from './Button';
|
||||
|
||||
describe('Button', () => {
|
||||
it('renders with correct text', () => {
|
||||
render(<Button>Click me</Button>);
|
||||
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClick when clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleClick = jest.fn();
|
||||
|
||||
render(<Button onClick={handleClick}>Click me</Button>);
|
||||
await user.click(screen.getByRole('button'));
|
||||
|
||||
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('is disabled when loading', () => {
|
||||
render(<Button isLoading>Submit</Button>);
|
||||
expect(screen.getByRole('button')).toBeDisabled();
|
||||
expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true');
|
||||
});
|
||||
|
||||
it('shows loading text when loading', () => {
|
||||
render(<Button isLoading loadingText="Submitting...">Submit</Button>);
|
||||
expect(screen.getByText('Submitting...')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Hook Testing
|
||||
|
||||
```tsx
|
||||
// useCounter.test.ts
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useCounter } from './useCounter';
|
||||
|
||||
describe('useCounter', () => {
|
||||
it('initializes with default value', () => {
|
||||
const { result } = renderHook(() => useCounter());
|
||||
expect(result.current.count).toBe(0);
|
||||
});
|
||||
|
||||
it('initializes with custom value', () => {
|
||||
const { result } = renderHook(() => useCounter(10));
|
||||
expect(result.current.count).toBe(10);
|
||||
});
|
||||
|
||||
it('increments count', () => {
|
||||
const { result } = renderHook(() => useCounter());
|
||||
|
||||
act(() => {
|
||||
result.current.increment();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(1);
|
||||
});
|
||||
|
||||
it('resets to initial value', () => {
|
||||
const { result } = renderHook(() => useCounter(5));
|
||||
|
||||
act(() => {
|
||||
result.current.increment();
|
||||
result.current.increment();
|
||||
result.current.reset();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(5);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
|
||||
```tsx
|
||||
// LoginForm.test.tsx
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { LoginForm } from './LoginForm';
|
||||
import { AuthProvider } from '@/contexts/AuthContext';
|
||||
|
||||
const mockLogin = jest.fn();
|
||||
|
||||
jest.mock('@/lib/auth', () => ({
|
||||
login: (...args: unknown[]) => mockLogin(...args),
|
||||
}));
|
||||
|
||||
describe('LoginForm', () => {
|
||||
beforeEach(() => {
|
||||
mockLogin.mockReset();
|
||||
});
|
||||
|
||||
it('submits form with valid credentials', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockLogin.mockResolvedValueOnce({ user: { id: '1', name: 'Test' } });
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
<LoginForm />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
|
||||
await user.type(screen.getByLabelText(/password/i), 'password123');
|
||||
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockLogin).toHaveBeenCalledWith('test@example.com', 'password123');
|
||||
});
|
||||
});
|
||||
|
||||
it('shows validation errors for empty fields', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<AuthProvider>
|
||||
<LoginForm />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
||||
|
||||
expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/password is required/i)).toBeInTheDocument();
|
||||
expect(mockLogin).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### E2E Testing with Playwright
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Advanced example
|
||||
async function advancedExample() {
|
||||
// Code here
|
||||
// e2e/checkout.spec.ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Checkout flow', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.click('[data-testid="product-1"] button');
|
||||
await page.click('[data-testid="cart-button"]');
|
||||
});
|
||||
|
||||
test('completes checkout with valid payment', async ({ page }) => {
|
||||
await page.click('text=Proceed to Checkout');
|
||||
|
||||
// Fill shipping info
|
||||
await page.fill('[name="email"]', 'test@example.com');
|
||||
await page.fill('[name="address"]', '123 Test St');
|
||||
await page.fill('[name="city"]', 'Test City');
|
||||
await page.selectOption('[name="state"]', 'CA');
|
||||
await page.fill('[name="zip"]', '90210');
|
||||
|
||||
await page.click('text=Continue to Payment');
|
||||
await page.click('text=Place Order');
|
||||
|
||||
// Verify success
|
||||
await expect(page).toHaveURL(/\/order\/confirmation/);
|
||||
await expect(page.locator('h1')).toHaveText('Order Confirmed!');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TypeScript Patterns
|
||||
|
||||
### Component Props
|
||||
|
||||
```tsx
|
||||
// Use interface for component props
|
||||
interface ButtonProps {
|
||||
variant?: 'primary' | 'secondary' | 'ghost';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
isLoading?: boolean;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
// Extend HTML attributes
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary';
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
function Button({ variant = 'primary', isLoading, children, ...props }: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
disabled={props.disabled || isLoading}
|
||||
className={cn(variants[variant], props.className)}
|
||||
>
|
||||
{isLoading ? <Spinner /> : children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Polymorphic components
|
||||
type PolymorphicProps<E extends React.ElementType> = {
|
||||
as?: E;
|
||||
} & React.ComponentPropsWithoutRef<E>;
|
||||
|
||||
function Box<E extends React.ElementType = 'div'>({
|
||||
as,
|
||||
children,
|
||||
...props
|
||||
}: PolymorphicProps<E>) {
|
||||
const Component = as || 'div';
|
||||
return <Component {...props}>{children}</Component>;
|
||||
}
|
||||
|
||||
// Usage
|
||||
<Box as="section" id="hero">Content</Box>
|
||||
<Box as="article">Article content</Box>
|
||||
```
|
||||
|
||||
### Discriminated Unions
|
||||
|
||||
```tsx
|
||||
// State machines with exhaustive type checking
|
||||
type AsyncState<T> =
|
||||
| { status: 'idle' }
|
||||
| { status: 'loading' }
|
||||
| { status: 'success'; data: T }
|
||||
| { status: 'error'; error: Error };
|
||||
|
||||
function DataDisplay<T>({ state, render }: {
|
||||
state: AsyncState<T>;
|
||||
render: (data: T) => React.ReactNode;
|
||||
}) {
|
||||
switch (state.status) {
|
||||
case 'idle':
|
||||
return null;
|
||||
case 'loading':
|
||||
return <Spinner />;
|
||||
case 'success':
|
||||
return <>{render(state.data)}</>;
|
||||
case 'error':
|
||||
return <ErrorMessage error={state.error} />;
|
||||
// TypeScript ensures all cases are handled
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Guidelines
|
||||
### Generic Components
|
||||
|
||||
### Code Organization
|
||||
- Clear structure
|
||||
- Logical separation
|
||||
- Consistent naming
|
||||
- Proper documentation
|
||||
```tsx
|
||||
// Generic list component
|
||||
interface ListProps<T> {
|
||||
items: T[];
|
||||
renderItem: (item: T, index: number) => React.ReactNode;
|
||||
keyExtractor: (item: T) => string;
|
||||
emptyMessage?: string;
|
||||
}
|
||||
|
||||
### Performance Considerations
|
||||
- Optimization strategies
|
||||
- Bottleneck identification
|
||||
- Monitoring approaches
|
||||
- Scaling techniques
|
||||
function List<T>({ items, renderItem, keyExtractor, emptyMessage }: ListProps<T>) {
|
||||
if (items.length === 0) {
|
||||
return <p className="text-muted">{emptyMessage || 'No items'}</p>;
|
||||
}
|
||||
|
||||
### Security Best Practices
|
||||
- Input validation
|
||||
- Authentication
|
||||
- Authorization
|
||||
- Data protection
|
||||
return (
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
## Common Patterns
|
||||
// Usage
|
||||
<List
|
||||
items={users}
|
||||
keyExtractor={(user) => user.id}
|
||||
renderItem={(user) => <UserCard user={user} />}
|
||||
/>
|
||||
```
|
||||
|
||||
### Pattern A
|
||||
Implementation details and examples.
|
||||
### Type Guards
|
||||
|
||||
### Pattern B
|
||||
Implementation details and examples.
|
||||
```tsx
|
||||
// User-defined type guards
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
### Pattern C
|
||||
Implementation details and examples.
|
||||
interface Admin extends User {
|
||||
role: 'admin';
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
function isAdmin(user: User): user is Admin {
|
||||
return 'role' in user && user.role === 'admin';
|
||||
}
|
||||
|
||||
### Anti-Pattern 1
|
||||
What not to do and why.
|
||||
function UserBadge({ user }: { user: User }) {
|
||||
if (isAdmin(user)) {
|
||||
// TypeScript knows user is Admin here
|
||||
return <Badge variant="admin">Admin ({user.permissions.length} perms)</Badge>;
|
||||
}
|
||||
|
||||
### Anti-Pattern 2
|
||||
What not to do and why.
|
||||
return <Badge>User</Badge>;
|
||||
}
|
||||
|
||||
## Tools and Resources
|
||||
// API response type guards
|
||||
interface ApiSuccess<T> {
|
||||
success: true;
|
||||
data: T;
|
||||
}
|
||||
|
||||
### Recommended Tools
|
||||
- Tool 1: Purpose
|
||||
- Tool 2: Purpose
|
||||
- Tool 3: Purpose
|
||||
interface ApiError {
|
||||
success: false;
|
||||
error: string;
|
||||
}
|
||||
|
||||
### Further Reading
|
||||
- Resource 1
|
||||
- Resource 2
|
||||
- Resource 3
|
||||
type ApiResponse<T> = ApiSuccess<T> | ApiError;
|
||||
|
||||
## Conclusion
|
||||
function isApiSuccess<T>(response: ApiResponse<T>): response is ApiSuccess<T> {
|
||||
return response.success === true;
|
||||
}
|
||||
```
|
||||
|
||||
Key takeaways for using this reference guide effectively.
|
||||
---
|
||||
|
||||
## Tailwind CSS
|
||||
|
||||
### Component Variants with CVA
|
||||
|
||||
```tsx
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
// Base styles
|
||||
'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus-visible:ring-blue-500',
|
||||
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:ring-gray-500',
|
||||
ghost: 'hover:bg-gray-100 hover:text-gray-900',
|
||||
destructive: 'bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-500',
|
||||
},
|
||||
size: {
|
||||
sm: 'h-8 px-3 text-sm',
|
||||
md: 'h-10 px-4 text-sm',
|
||||
lg: 'h-12 px-6 text-base',
|
||||
icon: 'h-10 w-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
|
||||
function Button({ className, variant, size, ...props }: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={cn(buttonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
<Button variant="primary" size="lg">Large Primary</Button>
|
||||
<Button variant="ghost" size="icon"><MenuIcon /></Button>
|
||||
```
|
||||
|
||||
### Responsive Design
|
||||
|
||||
```tsx
|
||||
// Mobile-first responsive design
|
||||
<div className="
|
||||
grid
|
||||
grid-cols-1 {/* Mobile: 1 column */}
|
||||
sm:grid-cols-2 {/* 640px+: 2 columns */}
|
||||
lg:grid-cols-3 {/* 1024px+: 3 columns */}
|
||||
xl:grid-cols-4 {/* 1280px+: 4 columns */}
|
||||
gap-4
|
||||
sm:gap-6
|
||||
lg:gap-8
|
||||
">
|
||||
{products.map(product => <ProductCard key={product.id} product={product} />)}
|
||||
</div>
|
||||
|
||||
// Container with responsive padding
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
Content
|
||||
</div>
|
||||
|
||||
// Hide/show based on breakpoint
|
||||
<nav className="hidden md:flex">Desktop nav</nav>
|
||||
<button className="md:hidden">Mobile menu</button>
|
||||
```
|
||||
|
||||
### Animation Utilities
|
||||
|
||||
```tsx
|
||||
// Skeleton loading
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4" />
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2" />
|
||||
</div>
|
||||
|
||||
// Transitions
|
||||
<button className="
|
||||
transition-all
|
||||
duration-200
|
||||
ease-in-out
|
||||
hover:scale-105
|
||||
active:scale-95
|
||||
">
|
||||
Hover me
|
||||
</button>
|
||||
|
||||
// Custom animations in tailwind.config.js
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.3s ease-out',
|
||||
'slide-up': 'slideUp 0.3s ease-out',
|
||||
'spin-slow': 'spin 3s linear infinite',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Usage
|
||||
<div className="animate-fade-in">Fading in</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Feature-Based Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router
|
||||
│ ├── (auth)/ # Auth route group
|
||||
│ │ ├── login/
|
||||
│ │ └── register/
|
||||
│ ├── dashboard/
|
||||
│ │ ├── page.tsx
|
||||
│ │ └── layout.tsx
|
||||
│ └── layout.tsx
|
||||
├── components/
|
||||
│ ├── ui/ # Shared UI components
|
||||
│ │ ├── Button.tsx
|
||||
│ │ ├── Input.tsx
|
||||
│ │ └── index.ts
|
||||
│ └── features/ # Feature-specific components
|
||||
│ ├── auth/
|
||||
│ │ ├── LoginForm.tsx
|
||||
│ │ └── RegisterForm.tsx
|
||||
│ └── dashboard/
|
||||
│ ├── StatsCard.tsx
|
||||
│ └── RecentActivity.tsx
|
||||
├── hooks/ # Custom React hooks
|
||||
│ ├── useAuth.ts
|
||||
│ ├── useDebounce.ts
|
||||
│ └── useLocalStorage.ts
|
||||
├── lib/ # Utilities and configs
|
||||
│ ├── utils.ts
|
||||
│ ├── api.ts
|
||||
│ └── constants.ts
|
||||
├── types/ # TypeScript types
|
||||
│ ├── user.ts
|
||||
│ └── api.ts
|
||||
└── styles/
|
||||
└── globals.css
|
||||
```
|
||||
|
||||
### Barrel Exports
|
||||
|
||||
```tsx
|
||||
// components/ui/index.ts
|
||||
export { Button } from './Button';
|
||||
export { Input } from './Input';
|
||||
export { Card, CardHeader, CardContent, CardFooter } from './Card';
|
||||
export { Dialog, DialogTrigger, DialogContent } from './Dialog';
|
||||
|
||||
// Usage
|
||||
import { Button, Input, Card } from '@/components/ui';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
### XSS Prevention
|
||||
|
||||
React escapes content by default, which prevents most XSS attacks. When you need to render HTML content:
|
||||
|
||||
1. **Avoid rendering raw HTML** when possible
|
||||
2. **Sanitize with DOMPurify** for trusted content sources
|
||||
3. **Use allow-lists** for permitted tags and attributes
|
||||
|
||||
```tsx
|
||||
// React escapes by default - this is safe
|
||||
<div>{userInput}</div>
|
||||
|
||||
// When you must render HTML, sanitize first
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
function SafeHTML({ html }: { html: string }) {
|
||||
const sanitized = DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
|
||||
ALLOWED_ATTR: ['href'],
|
||||
});
|
||||
|
||||
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Input Validation
|
||||
|
||||
```tsx
|
||||
import { z } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
password: z.string()
|
||||
.min(8, 'Password must be at least 8 characters')
|
||||
.regex(/[A-Z]/, 'Password must contain uppercase letter')
|
||||
.regex(/[0-9]/, 'Password must contain number'),
|
||||
confirmPassword: z.string(),
|
||||
}).refine((data) => data.password === data.confirmPassword, {
|
||||
message: 'Passwords do not match',
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof schema>;
|
||||
|
||||
function RegisterForm() {
|
||||
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Input {...register('email')} error={errors.email?.message} />
|
||||
<Input type="password" {...register('password')} error={errors.password?.message} />
|
||||
<Input type="password" {...register('confirmPassword')} error={errors.confirmPassword?.message} />
|
||||
<Button type="submit">Register</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Secure API Calls
|
||||
|
||||
```tsx
|
||||
// Use environment variables for API endpoints
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// Never include secrets in client code - use server-side API routes
|
||||
// app/api/data/route.ts
|
||||
export async function GET() {
|
||||
const response = await fetch('https://api.example.com/data', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.API_SECRET}`, // Server-side only
|
||||
},
|
||||
});
|
||||
|
||||
return Response.json(await response.json());
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,103 +1,724 @@
|
||||
# Nextjs Optimization Guide
|
||||
# Next.js Optimization Guide
|
||||
|
||||
## Overview
|
||||
Performance optimization techniques for Next.js 14+ applications.
|
||||
|
||||
This reference guide provides comprehensive information for senior frontend.
|
||||
---
|
||||
|
||||
## Patterns and Practices
|
||||
## Table of Contents
|
||||
|
||||
### Pattern 1: Best Practice Implementation
|
||||
- [Rendering Strategies](#rendering-strategies)
|
||||
- [Image Optimization](#image-optimization)
|
||||
- [Code Splitting](#code-splitting)
|
||||
- [Data Fetching](#data-fetching)
|
||||
- [Caching Strategies](#caching-strategies)
|
||||
- [Bundle Optimization](#bundle-optimization)
|
||||
- [Core Web Vitals](#core-web-vitals)
|
||||
|
||||
**Description:**
|
||||
Detailed explanation of the pattern.
|
||||
---
|
||||
|
||||
**When to Use:**
|
||||
- Scenario 1
|
||||
- Scenario 2
|
||||
- Scenario 3
|
||||
## Rendering Strategies
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Example code implementation
|
||||
export class Example {
|
||||
// Implementation details
|
||||
### Server Components (Default)
|
||||
|
||||
Server Components render on the server and send HTML to the client. Use for data-heavy, non-interactive content.
|
||||
|
||||
```tsx
|
||||
// app/products/page.tsx - Server Component (default)
|
||||
async function ProductsPage() {
|
||||
// This runs on the server - no client bundle impact
|
||||
const products = await db.products.findMany();
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{products.map(product => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Benefit 1
|
||||
- Benefit 2
|
||||
- Benefit 3
|
||||
### Client Components
|
||||
|
||||
**Trade-offs:**
|
||||
- Consider 1
|
||||
- Consider 2
|
||||
- Consider 3
|
||||
Use `'use client'` only when you need:
|
||||
- Event handlers (onClick, onChange)
|
||||
- State (useState, useReducer)
|
||||
- Effects (useEffect)
|
||||
- Browser APIs (window, document)
|
||||
|
||||
### Pattern 2: Advanced Technique
|
||||
```tsx
|
||||
'use client';
|
||||
|
||||
**Description:**
|
||||
Another important pattern for senior frontend.
|
||||
import { useState } from 'react';
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Advanced example
|
||||
async function advancedExample() {
|
||||
// Code here
|
||||
function AddToCartButton({ productId }: { productId: string }) {
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
|
||||
async function handleClick() {
|
||||
setIsAdding(true);
|
||||
await addToCart(productId);
|
||||
setIsAdding(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleClick} disabled={isAdding}>
|
||||
{isAdding ? 'Adding...' : 'Add to Cart'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Guidelines
|
||||
### Mixing Server and Client Components
|
||||
|
||||
### Code Organization
|
||||
- Clear structure
|
||||
- Logical separation
|
||||
- Consistent naming
|
||||
- Proper documentation
|
||||
```tsx
|
||||
// app/products/[id]/page.tsx - Server Component
|
||||
async function ProductPage({ params }: { params: { id: string } }) {
|
||||
const product = await getProduct(params.id);
|
||||
|
||||
### Performance Considerations
|
||||
- Optimization strategies
|
||||
- Bottleneck identification
|
||||
- Monitoring approaches
|
||||
- Scaling techniques
|
||||
return (
|
||||
<div>
|
||||
{/* Server-rendered content */}
|
||||
<h1>{product.name}</h1>
|
||||
<p>{product.description}</p>
|
||||
|
||||
### Security Best Practices
|
||||
- Input validation
|
||||
- Authentication
|
||||
- Authorization
|
||||
- Data protection
|
||||
{/* Client component for interactivity */}
|
||||
<AddToCartButton productId={product.id} />
|
||||
|
||||
## Common Patterns
|
||||
{/* Server component for reviews */}
|
||||
<ProductReviews productId={product.id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern A
|
||||
Implementation details and examples.
|
||||
### Static vs Dynamic Rendering
|
||||
|
||||
### Pattern B
|
||||
Implementation details and examples.
|
||||
```tsx
|
||||
// Force static generation at build time
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
### Pattern C
|
||||
Implementation details and examples.
|
||||
// Force dynamic rendering at request time
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
// Revalidate every 60 seconds (ISR)
|
||||
export const revalidate = 60;
|
||||
|
||||
### Anti-Pattern 1
|
||||
What not to do and why.
|
||||
// Revalidate on-demand
|
||||
import { revalidatePath, revalidateTag } from 'next/cache';
|
||||
|
||||
### Anti-Pattern 2
|
||||
What not to do and why.
|
||||
async function updateProduct(id: string, data: ProductData) {
|
||||
await db.products.update({ where: { id }, data });
|
||||
|
||||
## Tools and Resources
|
||||
// Revalidate specific path
|
||||
revalidatePath(`/products/${id}`);
|
||||
|
||||
### Recommended Tools
|
||||
- Tool 1: Purpose
|
||||
- Tool 2: Purpose
|
||||
- Tool 3: Purpose
|
||||
// Or revalidate by tag
|
||||
revalidateTag('products');
|
||||
}
|
||||
```
|
||||
|
||||
### Further Reading
|
||||
- Resource 1
|
||||
- Resource 2
|
||||
- Resource 3
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
## Image Optimization
|
||||
|
||||
Key takeaways for using this reference guide effectively.
|
||||
### Next.js Image Component
|
||||
|
||||
```tsx
|
||||
import Image from 'next/image';
|
||||
|
||||
// Basic optimized image
|
||||
<Image
|
||||
src="/hero.jpg"
|
||||
alt="Hero image"
|
||||
width={1200}
|
||||
height={600}
|
||||
priority // Load immediately for LCP
|
||||
/>
|
||||
|
||||
// Responsive image
|
||||
<Image
|
||||
src="/product.jpg"
|
||||
alt="Product"
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
|
||||
// With placeholder blur
|
||||
import productImage from '@/public/product.jpg';
|
||||
|
||||
<Image
|
||||
src={productImage}
|
||||
alt="Product"
|
||||
placeholder="blur" // Uses imported image data
|
||||
/>
|
||||
```
|
||||
|
||||
### Remote Images Configuration
|
||||
|
||||
```js
|
||||
// next.config.js
|
||||
module.exports = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'cdn.example.com',
|
||||
pathname: '/images/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '*.cloudinary.com',
|
||||
},
|
||||
],
|
||||
// Image formats (webp is default)
|
||||
formats: ['image/avif', 'image/webp'],
|
||||
// Device sizes for srcset
|
||||
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
|
||||
// Image sizes for srcset
|
||||
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Lazy Loading Patterns
|
||||
|
||||
```tsx
|
||||
// Images below the fold - lazy load (default)
|
||||
<Image
|
||||
src="/gallery/photo1.jpg"
|
||||
alt="Gallery photo"
|
||||
width={400}
|
||||
height={300}
|
||||
loading="lazy" // Default behavior
|
||||
/>
|
||||
|
||||
// Above the fold - load immediately
|
||||
<Image
|
||||
src="/hero.jpg"
|
||||
alt="Hero"
|
||||
width={1200}
|
||||
height={600}
|
||||
priority
|
||||
loading="eager"
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Splitting
|
||||
|
||||
### Dynamic Imports
|
||||
|
||||
```tsx
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
// Basic dynamic import
|
||||
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
|
||||
loading: () => <ChartSkeleton />,
|
||||
});
|
||||
|
||||
// Disable SSR for client-only components
|
||||
const MapComponent = dynamic(() => import('@/components/Map'), {
|
||||
ssr: false,
|
||||
loading: () => <div className="h-[400px] bg-gray-100" />,
|
||||
});
|
||||
|
||||
// Named exports
|
||||
const Modal = dynamic(() =>
|
||||
import('@/components/ui').then(mod => mod.Modal)
|
||||
);
|
||||
|
||||
// With suspense
|
||||
const DashboardCharts = dynamic(() => import('@/components/DashboardCharts'), {
|
||||
loading: () => <Suspense fallback={<ChartsSkeleton />} />,
|
||||
});
|
||||
```
|
||||
|
||||
### Route-Based Splitting
|
||||
|
||||
```tsx
|
||||
// app/dashboard/analytics/page.tsx
|
||||
// This page only loads when /dashboard/analytics is visited
|
||||
import { Suspense } from 'react';
|
||||
import AnalyticsCharts from './AnalyticsCharts';
|
||||
|
||||
export default function AnalyticsPage() {
|
||||
return (
|
||||
<Suspense fallback={<AnalyticsSkeleton />}>
|
||||
<AnalyticsCharts />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Parallel Routes for Code Splitting
|
||||
|
||||
```
|
||||
app/
|
||||
├── dashboard/
|
||||
│ ├── @analytics/
|
||||
│ │ └── page.tsx # Loaded in parallel
|
||||
│ ├── @metrics/
|
||||
│ │ └── page.tsx # Loaded in parallel
|
||||
│ ├── layout.tsx
|
||||
│ └── page.tsx
|
||||
```
|
||||
|
||||
```tsx
|
||||
// app/dashboard/layout.tsx
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
analytics,
|
||||
metrics,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
analytics: React.ReactNode;
|
||||
metrics: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{children}
|
||||
<Suspense fallback={<AnalyticsSkeleton />}>{analytics}</Suspense>
|
||||
<Suspense fallback={<MetricsSkeleton />}>{metrics}</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Fetching
|
||||
|
||||
### Server-Side Data Fetching
|
||||
|
||||
```tsx
|
||||
// Parallel data fetching
|
||||
async function Dashboard() {
|
||||
// Start both requests simultaneously
|
||||
const [user, stats, notifications] = await Promise.all([
|
||||
getUser(),
|
||||
getStats(),
|
||||
getNotifications(),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<UserHeader user={user} />
|
||||
<StatsPanel stats={stats} />
|
||||
<NotificationList notifications={notifications} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming with Suspense
|
||||
|
||||
```tsx
|
||||
import { Suspense } from 'react';
|
||||
|
||||
async function ProductPage({ params }: { params: { id: string } }) {
|
||||
const product = await getProduct(params.id);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Immediate content */}
|
||||
<h1>{product.name}</h1>
|
||||
<p>{product.description}</p>
|
||||
|
||||
{/* Stream reviews - don't block page */}
|
||||
<Suspense fallback={<ReviewsSkeleton />}>
|
||||
<Reviews productId={params.id} />
|
||||
</Suspense>
|
||||
|
||||
{/* Stream recommendations */}
|
||||
<Suspense fallback={<RecommendationsSkeleton />}>
|
||||
<Recommendations productId={params.id} />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Slow data component
|
||||
async function Reviews({ productId }: { productId: string }) {
|
||||
const reviews = await getReviews(productId); // Slow query
|
||||
return <ReviewList reviews={reviews} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Request Memoization
|
||||
|
||||
```tsx
|
||||
// Next.js automatically dedupes identical requests
|
||||
async function Layout({ children }) {
|
||||
const user = await getUser(); // Request 1
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
async function Header() {
|
||||
const user = await getUser(); // Same request - cached!
|
||||
return <div>Hello, {user.name}</div>;
|
||||
}
|
||||
|
||||
// Both components call getUser() but only one request is made
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
### Fetch Cache Options
|
||||
|
||||
```tsx
|
||||
// Cache indefinitely (default for static)
|
||||
fetch('https://api.example.com/data');
|
||||
|
||||
// No cache - always fresh
|
||||
fetch('https://api.example.com/data', { cache: 'no-store' });
|
||||
|
||||
// Revalidate after time
|
||||
fetch('https://api.example.com/data', {
|
||||
next: { revalidate: 3600 } // 1 hour
|
||||
});
|
||||
|
||||
// Tag-based revalidation
|
||||
fetch('https://api.example.com/products', {
|
||||
next: { tags: ['products'] }
|
||||
});
|
||||
|
||||
// Later, revalidate by tag
|
||||
import { revalidateTag } from 'next/cache';
|
||||
revalidateTag('products');
|
||||
```
|
||||
|
||||
### Route Segment Config
|
||||
|
||||
```tsx
|
||||
// app/products/page.tsx
|
||||
|
||||
// Revalidate every hour
|
||||
export const revalidate = 3600;
|
||||
|
||||
// Or force dynamic
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// Generate static params at build
|
||||
export async function generateStaticParams() {
|
||||
const products = await getProducts();
|
||||
return products.map(p => ({ id: p.id }));
|
||||
}
|
||||
```
|
||||
|
||||
### unstable_cache for Custom Caching
|
||||
|
||||
```tsx
|
||||
import { unstable_cache } from 'next/cache';
|
||||
|
||||
const getCachedUser = unstable_cache(
|
||||
async (userId: string) => {
|
||||
const user = await db.users.findUnique({ where: { id: userId } });
|
||||
return user;
|
||||
},
|
||||
['user-cache'],
|
||||
{
|
||||
revalidate: 3600, // 1 hour
|
||||
tags: ['users'],
|
||||
}
|
||||
);
|
||||
|
||||
// Usage
|
||||
const user = await getCachedUser(userId);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bundle Optimization
|
||||
|
||||
### Analyze Bundle Size
|
||||
|
||||
```bash
|
||||
# Install analyzer
|
||||
npm install @next/bundle-analyzer
|
||||
|
||||
# Update next.config.js
|
||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
});
|
||||
|
||||
module.exports = withBundleAnalyzer({
|
||||
// config
|
||||
});
|
||||
|
||||
# Run analysis
|
||||
ANALYZE=true npm run build
|
||||
```
|
||||
|
||||
### Tree Shaking Imports
|
||||
|
||||
```tsx
|
||||
// BAD - Imports entire library
|
||||
import _ from 'lodash';
|
||||
const result = _.debounce(fn, 300);
|
||||
|
||||
// GOOD - Import only what you need
|
||||
import debounce from 'lodash/debounce';
|
||||
const result = debounce(fn, 300);
|
||||
|
||||
// GOOD - Named imports (tree-shakeable)
|
||||
import { debounce } from 'lodash-es';
|
||||
```
|
||||
|
||||
### Optimize Dependencies
|
||||
|
||||
```js
|
||||
// next.config.js
|
||||
module.exports = {
|
||||
// Transpile specific packages
|
||||
transpilePackages: ['ui-library', 'shared-utils'],
|
||||
|
||||
// Optimize package imports
|
||||
experimental: {
|
||||
optimizePackageImports: ['lucide-react', '@heroicons/react'],
|
||||
},
|
||||
|
||||
// External packages for server
|
||||
serverExternalPackages: ['sharp', 'bcrypt'],
|
||||
};
|
||||
```
|
||||
|
||||
### Font Optimization
|
||||
|
||||
```tsx
|
||||
// app/layout.tsx
|
||||
import { Inter, Roboto_Mono } from 'next/font/google';
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
variable: '--font-inter',
|
||||
});
|
||||
|
||||
const robotoMono = Roboto_Mono({
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
variable: '--font-roboto-mono',
|
||||
});
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
|
||||
<body className="font-sans">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Web Vitals
|
||||
|
||||
### Largest Contentful Paint (LCP)
|
||||
|
||||
```tsx
|
||||
// Optimize LCP hero image
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<section className="relative h-[600px]">
|
||||
<Image
|
||||
src="/hero.jpg"
|
||||
alt="Hero"
|
||||
fill
|
||||
priority // Preload for LCP
|
||||
sizes="100vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="relative z-10">
|
||||
<h1>Welcome</h1>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Preload critical resources in layout
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<link rel="preload" href="/hero.jpg" as="image" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Cumulative Layout Shift (CLS)
|
||||
|
||||
```tsx
|
||||
// Prevent CLS with explicit dimensions
|
||||
<Image
|
||||
src="/product.jpg"
|
||||
alt="Product"
|
||||
width={400}
|
||||
height={300}
|
||||
/>
|
||||
|
||||
// Or use aspect ratio
|
||||
<div className="aspect-video relative">
|
||||
<Image src="/video-thumb.jpg" alt="Video" fill />
|
||||
</div>
|
||||
|
||||
// Skeleton placeholders
|
||||
function ProductCard({ product }: { product?: Product }) {
|
||||
if (!product) {
|
||||
return (
|
||||
<div className="animate-pulse">
|
||||
<div className="h-48 bg-gray-200 rounded" />
|
||||
<div className="h-4 bg-gray-200 rounded mt-2 w-3/4" />
|
||||
<div className="h-4 bg-gray-200 rounded mt-1 w-1/2" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Image src={product.image} alt={product.name} width={300} height={200} />
|
||||
<h3>{product.name}</h3>
|
||||
<p>{product.price}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### First Input Delay (FID) / Interaction to Next Paint (INP)
|
||||
|
||||
```tsx
|
||||
// Defer non-critical JavaScript
|
||||
import Script from 'next/script';
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{children}
|
||||
|
||||
{/* Load analytics after page is interactive */}
|
||||
<Script
|
||||
src="https://analytics.example.com/script.js"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
|
||||
{/* Load chat widget when idle */}
|
||||
<Script
|
||||
src="https://chat.example.com/widget.js"
|
||||
strategy="lazyOnload"
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
// Use web workers for heavy computation
|
||||
// app/components/DataProcessor.tsx
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
function DataProcessor({ data }: { data: number[] }) {
|
||||
const [result, setResult] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const worker = new Worker(new URL('../workers/processor.js', import.meta.url));
|
||||
|
||||
worker.postMessage(data);
|
||||
worker.onmessage = (e) => setResult(e.data);
|
||||
|
||||
return () => worker.terminate();
|
||||
}, [data]);
|
||||
|
||||
return <div>Result: {result}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Measuring Performance
|
||||
|
||||
```tsx
|
||||
// app/components/PerformanceMonitor.tsx
|
||||
'use client';
|
||||
|
||||
import { useReportWebVitals } from 'next/web-vitals';
|
||||
|
||||
export function PerformanceMonitor() {
|
||||
useReportWebVitals((metric) => {
|
||||
switch (metric.name) {
|
||||
case 'LCP':
|
||||
console.log('LCP:', metric.value);
|
||||
break;
|
||||
case 'FID':
|
||||
console.log('FID:', metric.value);
|
||||
break;
|
||||
case 'CLS':
|
||||
console.log('CLS:', metric.value);
|
||||
break;
|
||||
case 'TTFB':
|
||||
console.log('TTFB:', metric.value);
|
||||
break;
|
||||
}
|
||||
|
||||
// Send to analytics
|
||||
analytics.track('web-vital', {
|
||||
name: metric.name,
|
||||
value: metric.value,
|
||||
id: metric.id,
|
||||
});
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Performance Checklist
|
||||
|
||||
| Area | Optimization | Impact |
|
||||
|------|-------------|--------|
|
||||
| Images | Use next/image with priority for LCP | High |
|
||||
| Fonts | Use next/font with display: swap | Medium |
|
||||
| Code | Dynamic imports for heavy components | High |
|
||||
| Data | Parallel fetching with Promise.all | High |
|
||||
| Render | Server Components by default | High |
|
||||
| Cache | Configure revalidate appropriately | Medium |
|
||||
| Bundle | Tree-shake imports, analyze size | Medium |
|
||||
|
||||
### Config Template
|
||||
|
||||
```js
|
||||
// next.config.js
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
images: {
|
||||
remotePatterns: [{ hostname: 'cdn.example.com' }],
|
||||
formats: ['image/avif', 'image/webp'],
|
||||
},
|
||||
experimental: {
|
||||
optimizePackageImports: ['lucide-react'],
|
||||
},
|
||||
headers: async () => [
|
||||
{
|
||||
source: '/(.*)',
|
||||
headers: [
|
||||
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
||||
{ key: 'X-Frame-Options', value: 'DENY' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
```
|
||||
|
||||
@@ -1,103 +1,746 @@
|
||||
# React Patterns
|
||||
|
||||
## Overview
|
||||
Production-ready patterns for building scalable React applications with TypeScript.
|
||||
|
||||
This reference guide provides comprehensive information for senior frontend.
|
||||
---
|
||||
|
||||
## Patterns and Practices
|
||||
## Table of Contents
|
||||
|
||||
### Pattern 1: Best Practice Implementation
|
||||
- [Component Composition](#component-composition)
|
||||
- [Custom Hooks](#custom-hooks)
|
||||
- [State Management](#state-management)
|
||||
- [Performance Patterns](#performance-patterns)
|
||||
- [Error Boundaries](#error-boundaries)
|
||||
- [Anti-Patterns](#anti-patterns)
|
||||
|
||||
**Description:**
|
||||
Detailed explanation of the pattern.
|
||||
---
|
||||
|
||||
**When to Use:**
|
||||
- Scenario 1
|
||||
- Scenario 2
|
||||
- Scenario 3
|
||||
## Component Composition
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Example code implementation
|
||||
export class Example {
|
||||
// Implementation details
|
||||
### Compound Components
|
||||
|
||||
Use compound components when building reusable UI components with multiple related parts.
|
||||
|
||||
```tsx
|
||||
// Compound component pattern for a Select
|
||||
interface SelectContextType {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const SelectContext = createContext<SelectContextType | null>(null);
|
||||
|
||||
function Select({ children, value, onChange }: {
|
||||
children: React.ReactNode;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<SelectContext.Provider value={{ value, onChange }}>
|
||||
<div className="relative">{children}</div>
|
||||
</SelectContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectTrigger({ children }: { children: React.ReactNode }) {
|
||||
const context = useContext(SelectContext);
|
||||
if (!context) throw new Error('SelectTrigger must be used within Select');
|
||||
|
||||
return (
|
||||
<button className="flex items-center gap-2 px-4 py-2 border rounded">
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectOption({ value, children }: { value: string; children: React.ReactNode }) {
|
||||
const context = useContext(SelectContext);
|
||||
if (!context) throw new Error('SelectOption must be used within Select');
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => context.onChange(value)}
|
||||
className={`px-4 py-2 cursor-pointer hover:bg-gray-100 ${
|
||||
context.value === value ? 'bg-blue-50' : ''
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Attach sub-components
|
||||
Select.Trigger = SelectTrigger;
|
||||
Select.Option = SelectOption;
|
||||
|
||||
// Usage
|
||||
<Select value={selected} onChange={setSelected}>
|
||||
<Select.Trigger>Choose option</Select.Trigger>
|
||||
<Select.Option value="a">Option A</Select.Option>
|
||||
<Select.Option value="b">Option B</Select.Option>
|
||||
</Select>
|
||||
```
|
||||
|
||||
### Render Props
|
||||
|
||||
Use render props when you need to share behavior with flexible rendering.
|
||||
|
||||
```tsx
|
||||
interface MousePosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
function MouseTracker({ render }: { render: (pos: MousePosition) => React.ReactNode }) {
|
||||
const [position, setPosition] = useState<MousePosition>({ x: 0, y: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
setPosition({ x: e.clientX, y: e.clientY });
|
||||
};
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
return () => window.removeEventListener('mousemove', handleMouseMove);
|
||||
}, []);
|
||||
|
||||
return <>{render(position)}</>;
|
||||
}
|
||||
|
||||
// Usage
|
||||
<MouseTracker
|
||||
render={({ x, y }) => (
|
||||
<div>Mouse position: {x}, {y}</div>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Higher-Order Components (HOC)
|
||||
|
||||
Use HOCs for cross-cutting concerns like authentication or logging.
|
||||
|
||||
```tsx
|
||||
function withAuth<P extends object>(WrappedComponent: React.ComponentType<P>) {
|
||||
return function AuthenticatedComponent(props: P) {
|
||||
const { user, isLoading } = useAuth();
|
||||
|
||||
if (isLoading) return <LoadingSpinner />;
|
||||
if (!user) return <Navigate to="/login" />;
|
||||
|
||||
return <WrappedComponent {...props} />;
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
const ProtectedDashboard = withAuth(Dashboard);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Hooks
|
||||
|
||||
### useAsync - Handle async operations
|
||||
|
||||
```tsx
|
||||
interface AsyncState<T> {
|
||||
data: T | null;
|
||||
error: Error | null;
|
||||
status: 'idle' | 'loading' | 'success' | 'error';
|
||||
}
|
||||
|
||||
function useAsync<T>(asyncFn: () => Promise<T>, deps: any[] = []) {
|
||||
const [state, setState] = useState<AsyncState<T>>({
|
||||
data: null,
|
||||
error: null,
|
||||
status: 'idle',
|
||||
});
|
||||
|
||||
const execute = useCallback(async () => {
|
||||
setState({ data: null, error: null, status: 'loading' });
|
||||
try {
|
||||
const data = await asyncFn();
|
||||
setState({ data, error: null, status: 'success' });
|
||||
} catch (error) {
|
||||
setState({ data: null, error: error as Error, status: 'error' });
|
||||
}
|
||||
}, deps);
|
||||
|
||||
useEffect(() => {
|
||||
execute();
|
||||
}, [execute]);
|
||||
|
||||
return { ...state, refetch: execute };
|
||||
}
|
||||
|
||||
// Usage
|
||||
function UserProfile({ userId }: { userId: string }) {
|
||||
const { data: user, status, error, refetch } = useAsync(
|
||||
() => fetchUser(userId),
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (status === 'loading') return <Spinner />;
|
||||
if (status === 'error') return <Error message={error?.message} />;
|
||||
if (!user) return null;
|
||||
|
||||
return <Profile user={user} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Benefit 1
|
||||
- Benefit 2
|
||||
- Benefit 3
|
||||
### useDebounce - Debounce values
|
||||
|
||||
**Trade-offs:**
|
||||
- Consider 1
|
||||
- Consider 2
|
||||
- Consider 3
|
||||
```tsx
|
||||
function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
### Pattern 2: Advanced Technique
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
||||
return () => clearTimeout(timer);
|
||||
}, [value, delay]);
|
||||
|
||||
**Description:**
|
||||
Another important pattern for senior frontend.
|
||||
return debouncedValue;
|
||||
}
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// Advanced example
|
||||
async function advancedExample() {
|
||||
// Code here
|
||||
// Usage
|
||||
function SearchInput() {
|
||||
const [query, setQuery] = useState('');
|
||||
const debouncedQuery = useDebounce(query, 300);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedQuery) {
|
||||
searchAPI(debouncedQuery);
|
||||
}
|
||||
}, [debouncedQuery]);
|
||||
|
||||
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Guidelines
|
||||
### useLocalStorage - Persist state
|
||||
|
||||
### Code Organization
|
||||
- Clear structure
|
||||
- Logical separation
|
||||
- Consistent naming
|
||||
- Proper documentation
|
||||
```tsx
|
||||
function useLocalStorage<T>(key: string, initialValue: T) {
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
if (typeof window === 'undefined') return initialValue;
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch {
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
|
||||
### Performance Considerations
|
||||
- Optimization strategies
|
||||
- Bottleneck identification
|
||||
- Monitoring approaches
|
||||
- Scaling techniques
|
||||
const setValue = useCallback((value: T | ((val: T) => T)) => {
|
||||
try {
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||
setStoredValue(valueToStore);
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving to localStorage:', error);
|
||||
}
|
||||
}, [key, storedValue]);
|
||||
|
||||
### Security Best Practices
|
||||
- Input validation
|
||||
- Authentication
|
||||
- Authorization
|
||||
- Data protection
|
||||
return [storedValue, setValue] as const;
|
||||
}
|
||||
|
||||
## Common Patterns
|
||||
// Usage
|
||||
const [theme, setTheme] = useLocalStorage('theme', 'light');
|
||||
```
|
||||
|
||||
### Pattern A
|
||||
Implementation details and examples.
|
||||
### useMediaQuery - Responsive design
|
||||
|
||||
### Pattern B
|
||||
Implementation details and examples.
|
||||
```tsx
|
||||
function useMediaQuery(query: string): boolean {
|
||||
const [matches, setMatches] = useState(false);
|
||||
|
||||
### Pattern C
|
||||
Implementation details and examples.
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(query);
|
||||
setMatches(media.matches);
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
|
||||
media.addEventListener('change', listener);
|
||||
return () => media.removeEventListener('change', listener);
|
||||
}, [query]);
|
||||
|
||||
### Anti-Pattern 1
|
||||
What not to do and why.
|
||||
return matches;
|
||||
}
|
||||
|
||||
### Anti-Pattern 2
|
||||
What not to do and why.
|
||||
// Usage
|
||||
function ResponsiveNav() {
|
||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||
return isMobile ? <MobileNav /> : <DesktopNav />;
|
||||
}
|
||||
```
|
||||
|
||||
## Tools and Resources
|
||||
### usePrevious - Track previous values
|
||||
|
||||
### Recommended Tools
|
||||
- Tool 1: Purpose
|
||||
- Tool 2: Purpose
|
||||
- Tool 3: Purpose
|
||||
```tsx
|
||||
function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = useRef<T>();
|
||||
|
||||
### Further Reading
|
||||
- Resource 1
|
||||
- Resource 2
|
||||
- Resource 3
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]);
|
||||
|
||||
## Conclusion
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
Key takeaways for using this reference guide effectively.
|
||||
// Usage
|
||||
function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
const prevCount = usePrevious(count);
|
||||
|
||||
return (
|
||||
<div>
|
||||
Current: {count}, Previous: {prevCount}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State Management
|
||||
|
||||
### Context with Reducer
|
||||
|
||||
For complex state that multiple components need to access.
|
||||
|
||||
```tsx
|
||||
// types.ts
|
||||
interface CartItem {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
interface CartState {
|
||||
items: CartItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
type CartAction =
|
||||
| { type: 'ADD_ITEM'; payload: CartItem }
|
||||
| { type: 'REMOVE_ITEM'; payload: string }
|
||||
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
|
||||
| { type: 'CLEAR_CART' };
|
||||
|
||||
// reducer.ts
|
||||
function cartReducer(state: CartState, action: CartAction): CartState {
|
||||
switch (action.type) {
|
||||
case 'ADD_ITEM': {
|
||||
const existingItem = state.items.find(i => i.id === action.payload.id);
|
||||
if (existingItem) {
|
||||
return {
|
||||
...state,
|
||||
items: state.items.map(item =>
|
||||
item.id === action.payload.id
|
||||
? { ...item, quantity: item.quantity + 1 }
|
||||
: item
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
items: [...state.items, { ...action.payload, quantity: 1 }],
|
||||
};
|
||||
}
|
||||
case 'REMOVE_ITEM':
|
||||
return {
|
||||
...state,
|
||||
items: state.items.filter(i => i.id !== action.payload),
|
||||
};
|
||||
case 'UPDATE_QUANTITY':
|
||||
return {
|
||||
...state,
|
||||
items: state.items.map(item =>
|
||||
item.id === action.payload.id
|
||||
? { ...item, quantity: action.payload.quantity }
|
||||
: item
|
||||
),
|
||||
};
|
||||
case 'CLEAR_CART':
|
||||
return { items: [], total: 0 };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// context.tsx
|
||||
const CartContext = createContext<{
|
||||
state: CartState;
|
||||
dispatch: React.Dispatch<CartAction>;
|
||||
} | null>(null);
|
||||
|
||||
function CartProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 });
|
||||
|
||||
// Compute total whenever items change
|
||||
const stateWithTotal = useMemo(() => ({
|
||||
...state,
|
||||
total: state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
||||
}), [state.items]);
|
||||
|
||||
return (
|
||||
<CartContext.Provider value={{ state: stateWithTotal, dispatch }}>
|
||||
{children}
|
||||
</CartContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useCart() {
|
||||
const context = useContext(CartContext);
|
||||
if (!context) throw new Error('useCart must be used within CartProvider');
|
||||
return context;
|
||||
}
|
||||
```
|
||||
|
||||
### Zustand (Lightweight Alternative)
|
||||
|
||||
```tsx
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
interface AuthStore {
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
const useAuthStore = create<AuthStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
user: null,
|
||||
token: null,
|
||||
login: async (email, password) => {
|
||||
const { user, token } = await authAPI.login(email, password);
|
||||
set({ user, token });
|
||||
},
|
||||
logout: () => set({ user: null, token: null }),
|
||||
}),
|
||||
{ name: 'auth-storage' }
|
||||
)
|
||||
);
|
||||
|
||||
// Usage
|
||||
function Profile() {
|
||||
const { user, logout } = useAuthStore();
|
||||
return user ? <div>{user.name} <button onClick={logout}>Logout</button></div> : null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Patterns
|
||||
|
||||
### React.memo with Custom Comparison
|
||||
|
||||
```tsx
|
||||
interface ListItemProps {
|
||||
item: { id: string; name: string; count: number };
|
||||
onSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
const ListItem = React.memo(
|
||||
function ListItem({ item, onSelect }: ListItemProps) {
|
||||
return (
|
||||
<div onClick={() => onSelect(item.id)}>
|
||||
{item.name} ({item.count})
|
||||
</div>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
// Only re-render if item data changed
|
||||
return (
|
||||
prevProps.item.id === nextProps.item.id &&
|
||||
prevProps.item.name === nextProps.item.name &&
|
||||
prevProps.item.count === nextProps.item.count
|
||||
);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### useMemo for Expensive Calculations
|
||||
|
||||
```tsx
|
||||
function DataTable({ data, sortColumn, filterText }: {
|
||||
data: Item[];
|
||||
sortColumn: string;
|
||||
filterText: string;
|
||||
}) {
|
||||
const processedData = useMemo(() => {
|
||||
// Filter
|
||||
let result = data.filter(item =>
|
||||
item.name.toLowerCase().includes(filterText.toLowerCase())
|
||||
);
|
||||
|
||||
// Sort
|
||||
result = [...result].sort((a, b) => {
|
||||
const aVal = a[sortColumn as keyof Item];
|
||||
const bVal = b[sortColumn as keyof Item];
|
||||
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [data, sortColumn, filterText]);
|
||||
|
||||
return (
|
||||
<table>
|
||||
{processedData.map(item => (
|
||||
<tr key={item.id}>{/* ... */}</tr>
|
||||
))}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### useCallback for Stable References
|
||||
|
||||
```tsx
|
||||
function ParentComponent() {
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
|
||||
// Stable reference - won't cause child re-renders
|
||||
const handleItemClick = useCallback((id: string) => {
|
||||
setItems(prev => prev.map(item =>
|
||||
item.id === id ? { ...item, selected: !item.selected } : item
|
||||
));
|
||||
}, []);
|
||||
|
||||
const handleAddItem = useCallback((newItem: Item) => {
|
||||
setItems(prev => [...prev, newItem]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ItemList items={items} onItemClick={handleItemClick} />
|
||||
<AddItemForm onAdd={handleAddItem} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Virtualization for Long Lists
|
||||
|
||||
```tsx
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
|
||||
function VirtualList({ items }: { items: Item[] }) {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: items.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 50, // estimated row height
|
||||
overscan: 5,
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={parentRef} className="h-[400px] overflow-auto">
|
||||
<div
|
||||
style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map(virtualRow => (
|
||||
<div
|
||||
key={virtualRow.key}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
{items[virtualRow.index].name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Boundaries
|
||||
|
||||
### Class-Based Error Boundary
|
||||
|
||||
```tsx
|
||||
interface ErrorBoundaryProps {
|
||||
children: React.ReactNode;
|
||||
fallback?: React.ReactNode;
|
||||
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
state: ErrorBoundaryState = { hasError: false, error: null };
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
this.props.onError?.(error, errorInfo);
|
||||
// Log to error reporting service
|
||||
console.error('Error caught:', error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return this.props.fallback || (
|
||||
<div className="p-4 bg-red-50 border border-red-200 rounded">
|
||||
<h2 className="text-red-800 font-bold">Something went wrong</h2>
|
||||
<p className="text-red-600">{this.state.error?.message}</p>
|
||||
<button
|
||||
onClick={() => this.setState({ hasError: false, error: null })}
|
||||
className="mt-2 px-4 py-2 bg-red-600 text-white rounded"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
<ErrorBoundary
|
||||
fallback={<ErrorFallback />}
|
||||
onError={(error) => trackError(error)}
|
||||
>
|
||||
<MyComponent />
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
### Suspense with Error Boundary
|
||||
|
||||
```tsx
|
||||
function DataComponent() {
|
||||
return (
|
||||
<ErrorBoundary fallback={<ErrorMessage />}>
|
||||
<Suspense fallback={<LoadingSpinner />}>
|
||||
<AsyncDataLoader />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Avoid: Inline Object/Array Creation in JSX
|
||||
|
||||
```tsx
|
||||
// BAD - Creates new object every render, causes re-renders
|
||||
<Component style={{ color: 'red' }} items={[1, 2, 3]} />
|
||||
|
||||
// GOOD - Define outside or use useMemo
|
||||
const style = { color: 'red' };
|
||||
const items = [1, 2, 3];
|
||||
<Component style={style} items={items} />
|
||||
|
||||
// Or with useMemo for dynamic values
|
||||
const style = useMemo(() => ({ color: theme.primary }), [theme.primary]);
|
||||
```
|
||||
|
||||
### Avoid: Index as Key for Dynamic Lists
|
||||
|
||||
```tsx
|
||||
// BAD - Index keys break with reordering/filtering
|
||||
{items.map((item, index) => (
|
||||
<Item key={index} data={item} />
|
||||
))}
|
||||
|
||||
// GOOD - Use stable unique ID
|
||||
{items.map(item => (
|
||||
<Item key={item.id} data={item} />
|
||||
))}
|
||||
```
|
||||
|
||||
### Avoid: Prop Drilling
|
||||
|
||||
```tsx
|
||||
// BAD - Passing props through many levels
|
||||
<App user={user}>
|
||||
<Layout user={user}>
|
||||
<Sidebar user={user}>
|
||||
<UserInfo user={user} />
|
||||
</Sidebar>
|
||||
</Layout>
|
||||
</App>
|
||||
|
||||
// GOOD - Use Context
|
||||
const UserContext = createContext<User | null>(null);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<UserContext.Provider value={user}>
|
||||
<Layout>
|
||||
<Sidebar>
|
||||
<UserInfo />
|
||||
</Sidebar>
|
||||
</Layout>
|
||||
</UserContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function UserInfo() {
|
||||
const user = useContext(UserContext);
|
||||
return <div>{user?.name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Avoid: Mutating State Directly
|
||||
|
||||
```tsx
|
||||
// BAD - Mutates state directly
|
||||
const addItem = (item: Item) => {
|
||||
items.push(item); // WRONG
|
||||
setItems(items); // Won't trigger re-render
|
||||
};
|
||||
|
||||
// GOOD - Create new array
|
||||
const addItem = (item: Item) => {
|
||||
setItems(prev => [...prev, item]);
|
||||
};
|
||||
|
||||
// GOOD - For objects
|
||||
const updateUser = (field: string, value: string) => {
|
||||
setUser(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
```
|
||||
|
||||
### Avoid: useEffect for Derived State
|
||||
|
||||
```tsx
|
||||
// BAD - Unnecessary effect and extra render
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setTotal(items.reduce((sum, item) => sum + item.price, 0));
|
||||
}, [items]);
|
||||
|
||||
// GOOD - Compute during render
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
const total = items.reduce((sum, item) => sum + item.price, 0);
|
||||
|
||||
// Or useMemo for expensive calculations
|
||||
const total = useMemo(
|
||||
() => items.reduce((sum, item) => sum + item.price, 0),
|
||||
[items]
|
||||
);
|
||||
```
|
||||
|
||||
@@ -1,114 +1,407 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Bundle Analyzer
|
||||
Automated tool for senior frontend tasks
|
||||
Frontend Bundle Analyzer
|
||||
|
||||
Analyzes package.json and project structure for bundle optimization opportunities,
|
||||
heavy dependencies, and best practice recommendations.
|
||||
|
||||
Usage:
|
||||
python bundle_analyzer.py <project_dir>
|
||||
python bundle_analyzer.py . --json
|
||||
python bundle_analyzer.py /path/to/project --verbose
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Optional, Any, Tuple
|
||||
|
||||
|
||||
# Known heavy packages and their lighter alternatives
|
||||
HEAVY_PACKAGES = {
|
||||
"moment": {
|
||||
"size": "290KB",
|
||||
"alternative": "date-fns (12KB) or dayjs (2KB)",
|
||||
"reason": "Large locale files bundled by default"
|
||||
},
|
||||
"lodash": {
|
||||
"size": "71KB",
|
||||
"alternative": "lodash-es with tree-shaking or individual imports (lodash/get)",
|
||||
"reason": "Full library often imported when only few functions needed"
|
||||
},
|
||||
"jquery": {
|
||||
"size": "87KB",
|
||||
"alternative": "Native DOM APIs or React/Vue patterns",
|
||||
"reason": "Rarely needed in modern frameworks"
|
||||
},
|
||||
"axios": {
|
||||
"size": "14KB",
|
||||
"alternative": "Native fetch API (0KB) or ky (3KB)",
|
||||
"reason": "Fetch API covers most use cases"
|
||||
},
|
||||
"underscore": {
|
||||
"size": "17KB",
|
||||
"alternative": "Native ES6+ methods or lodash-es",
|
||||
"reason": "Most utilities now in standard JavaScript"
|
||||
},
|
||||
"chart.js": {
|
||||
"size": "180KB",
|
||||
"alternative": "recharts (bundled with React) or lightweight-charts",
|
||||
"reason": "Consider if you need all chart types"
|
||||
},
|
||||
"three": {
|
||||
"size": "600KB",
|
||||
"alternative": "None - use dynamic import for 3D features",
|
||||
"reason": "Very large, should be lazy-loaded"
|
||||
},
|
||||
"firebase": {
|
||||
"size": "400KB+",
|
||||
"alternative": "Import specific modules (firebase/auth, firebase/firestore)",
|
||||
"reason": "Modular imports significantly reduce size"
|
||||
},
|
||||
"material-ui": {
|
||||
"size": "Large",
|
||||
"alternative": "shadcn/ui (copy-paste components) or Tailwind",
|
||||
"reason": "Heavy runtime, consider headless alternatives"
|
||||
},
|
||||
"@mui/material": {
|
||||
"size": "Large",
|
||||
"alternative": "shadcn/ui or Radix UI + Tailwind",
|
||||
"reason": "Heavy runtime, consider headless alternatives"
|
||||
},
|
||||
"antd": {
|
||||
"size": "Large",
|
||||
"alternative": "shadcn/ui or Radix UI + Tailwind",
|
||||
"reason": "Heavy runtime, consider headless alternatives"
|
||||
}
|
||||
}
|
||||
|
||||
# Recommended optimizations by package
|
||||
PACKAGE_OPTIMIZATIONS = {
|
||||
"react-icons": "Import individual icons: import { FaHome } from 'react-icons/fa'",
|
||||
"date-fns": "Use tree-shaking: import { format } from 'date-fns'",
|
||||
"@heroicons/react": "Already tree-shakeable, good choice",
|
||||
"lucide-react": "Already tree-shakeable, add to optimizePackageImports in next.config.js",
|
||||
"framer-motion": "Use dynamic import for non-critical animations",
|
||||
"recharts": "Consider lazy loading for dashboard charts",
|
||||
}
|
||||
|
||||
# Development dependencies that should not be in dependencies
|
||||
DEV_ONLY_PACKAGES = [
|
||||
"typescript", "@types/", "eslint", "prettier", "jest", "vitest",
|
||||
"@testing-library", "cypress", "playwright", "storybook", "@storybook",
|
||||
"webpack", "vite", "rollup", "esbuild", "tailwindcss", "postcss",
|
||||
"autoprefixer", "sass", "less", "husky", "lint-staged"
|
||||
]
|
||||
|
||||
|
||||
def load_package_json(project_dir: Path) -> Optional[Dict]:
|
||||
"""Load and parse package.json."""
|
||||
package_path = project_dir / "package.json"
|
||||
if not package_path.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(package_path) as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def analyze_dependencies(package_json: Dict) -> Dict:
|
||||
"""Analyze dependencies for issues."""
|
||||
deps = package_json.get("dependencies", {})
|
||||
dev_deps = package_json.get("devDependencies", {})
|
||||
|
||||
issues = []
|
||||
warnings = []
|
||||
optimizations = []
|
||||
|
||||
# Check for heavy packages
|
||||
for pkg, info in HEAVY_PACKAGES.items():
|
||||
if pkg in deps:
|
||||
issues.append({
|
||||
"package": pkg,
|
||||
"type": "heavy_dependency",
|
||||
"size": info["size"],
|
||||
"alternative": info["alternative"],
|
||||
"reason": info["reason"]
|
||||
})
|
||||
|
||||
# Check for dev dependencies in production
|
||||
for pkg in deps.keys():
|
||||
for dev_pattern in DEV_ONLY_PACKAGES:
|
||||
if dev_pattern in pkg:
|
||||
warnings.append({
|
||||
"package": pkg,
|
||||
"type": "dev_in_production",
|
||||
"message": f"{pkg} should be in devDependencies, not dependencies"
|
||||
})
|
||||
|
||||
# Check for optimization opportunities
|
||||
for pkg in deps.keys():
|
||||
for opt_pkg, opt_tip in PACKAGE_OPTIMIZATIONS.items():
|
||||
if opt_pkg in pkg:
|
||||
optimizations.append({
|
||||
"package": pkg,
|
||||
"tip": opt_tip
|
||||
})
|
||||
|
||||
# Check for outdated React patterns
|
||||
if "prop-types" in deps and ("typescript" in dev_deps or "@types/react" in dev_deps):
|
||||
warnings.append({
|
||||
"package": "prop-types",
|
||||
"type": "redundant",
|
||||
"message": "prop-types is redundant when using TypeScript"
|
||||
})
|
||||
|
||||
# Check for multiple state management libraries
|
||||
state_libs = ["redux", "@reduxjs/toolkit", "mobx", "zustand", "jotai", "recoil", "valtio"]
|
||||
found_state_libs = [lib for lib in state_libs if lib in deps]
|
||||
if len(found_state_libs) > 1:
|
||||
warnings.append({
|
||||
"packages": found_state_libs,
|
||||
"type": "multiple_state_libs",
|
||||
"message": f"Multiple state management libraries found: {', '.join(found_state_libs)}"
|
||||
})
|
||||
|
||||
return {
|
||||
"total_dependencies": len(deps),
|
||||
"total_dev_dependencies": len(dev_deps),
|
||||
"issues": issues,
|
||||
"warnings": warnings,
|
||||
"optimizations": optimizations
|
||||
}
|
||||
|
||||
|
||||
def check_nextjs_config(project_dir: Path) -> Dict:
|
||||
"""Check Next.js configuration for optimizations."""
|
||||
config_paths = [
|
||||
project_dir / "next.config.js",
|
||||
project_dir / "next.config.mjs",
|
||||
project_dir / "next.config.ts"
|
||||
]
|
||||
|
||||
for config_path in config_paths:
|
||||
if config_path.exists():
|
||||
try:
|
||||
content = config_path.read_text()
|
||||
suggestions = []
|
||||
|
||||
# Check for image optimization
|
||||
if "images" not in content:
|
||||
suggestions.append("Configure images.remotePatterns for optimized image loading")
|
||||
|
||||
# Check for package optimization
|
||||
if "optimizePackageImports" not in content:
|
||||
suggestions.append("Add experimental.optimizePackageImports for lucide-react, @heroicons/react")
|
||||
|
||||
# Check for transpilePackages
|
||||
if "transpilePackages" not in content and "swc" not in content:
|
||||
suggestions.append("Consider transpilePackages for monorepo packages")
|
||||
|
||||
return {
|
||||
"found": True,
|
||||
"path": str(config_path),
|
||||
"suggestions": suggestions
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"found": False,
|
||||
"suggestions": ["Create next.config.js with image and bundle optimizations"]
|
||||
}
|
||||
|
||||
|
||||
def analyze_imports(project_dir: Path) -> Dict:
|
||||
"""Analyze import patterns in source files."""
|
||||
issues = []
|
||||
src_dirs = [project_dir / "src", project_dir / "app", project_dir / "pages"]
|
||||
|
||||
patterns_to_check = [
|
||||
(r"import\s+\*\s+as\s+\w+\s+from\s+['\"]lodash['\"]", "Avoid import * from lodash, use individual imports"),
|
||||
(r"import\s+moment\s+from\s+['\"]moment['\"]", "Consider replacing moment with date-fns or dayjs"),
|
||||
(r"import\s+\{\s*\w+(?:,\s*\w+){5,}\s*\}\s+from\s+['\"]react-icons", "Import icons from specific icon sets (react-icons/fa)"),
|
||||
]
|
||||
|
||||
files_checked = 0
|
||||
for src_dir in src_dirs:
|
||||
if not src_dir.exists():
|
||||
continue
|
||||
|
||||
for ext in ["*.ts", "*.tsx", "*.js", "*.jsx"]:
|
||||
for file_path in src_dir.glob(f"**/{ext}"):
|
||||
if "node_modules" in str(file_path):
|
||||
continue
|
||||
|
||||
files_checked += 1
|
||||
try:
|
||||
content = file_path.read_text()
|
||||
for pattern, message in patterns_to_check:
|
||||
if re.search(pattern, content):
|
||||
issues.append({
|
||||
"file": str(file_path.relative_to(project_dir)),
|
||||
"issue": message
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return {
|
||||
"files_checked": files_checked,
|
||||
"issues": issues
|
||||
}
|
||||
|
||||
|
||||
def calculate_score(analysis: Dict) -> Tuple[int, str]:
|
||||
"""Calculate bundle health score."""
|
||||
score = 100
|
||||
|
||||
# Deduct for heavy dependencies
|
||||
score -= len(analysis["dependencies"]["issues"]) * 10
|
||||
|
||||
# Deduct for dev deps in production
|
||||
score -= len([w for w in analysis["dependencies"]["warnings"]
|
||||
if w.get("type") == "dev_in_production"]) * 5
|
||||
|
||||
# Deduct for import issues
|
||||
score -= len(analysis.get("imports", {}).get("issues", [])) * 3
|
||||
|
||||
# Deduct for missing Next.js optimizations
|
||||
if not analysis.get("nextjs", {}).get("found", True):
|
||||
score -= 10
|
||||
|
||||
score = max(0, min(100, score))
|
||||
|
||||
if score >= 90:
|
||||
grade = "A"
|
||||
elif score >= 80:
|
||||
grade = "B"
|
||||
elif score >= 70:
|
||||
grade = "C"
|
||||
elif score >= 60:
|
||||
grade = "D"
|
||||
else:
|
||||
grade = "F"
|
||||
|
||||
return score, grade
|
||||
|
||||
|
||||
def print_report(analysis: Dict) -> None:
|
||||
"""Print human-readable report."""
|
||||
score, grade = calculate_score(analysis)
|
||||
|
||||
print("=" * 60)
|
||||
print("FRONTEND BUNDLE ANALYSIS REPORT")
|
||||
print("=" * 60)
|
||||
print(f"\nBundle Health Score: {score}/100 ({grade})")
|
||||
|
||||
deps = analysis["dependencies"]
|
||||
print(f"\nDependencies: {deps['total_dependencies']} production, {deps['total_dev_dependencies']} dev")
|
||||
|
||||
# Heavy dependencies
|
||||
if deps["issues"]:
|
||||
print("\n--- HEAVY DEPENDENCIES ---")
|
||||
for issue in deps["issues"]:
|
||||
print(f"\n {issue['package']} ({issue['size']})")
|
||||
print(f" Reason: {issue['reason']}")
|
||||
print(f" Alternative: {issue['alternative']}")
|
||||
|
||||
# Warnings
|
||||
if deps["warnings"]:
|
||||
print("\n--- WARNINGS ---")
|
||||
for warning in deps["warnings"]:
|
||||
if "package" in warning:
|
||||
print(f" - {warning['package']}: {warning['message']}")
|
||||
else:
|
||||
print(f" - {warning['message']}")
|
||||
|
||||
# Optimizations
|
||||
if deps["optimizations"]:
|
||||
print("\n--- OPTIMIZATION TIPS ---")
|
||||
for opt in deps["optimizations"]:
|
||||
print(f" - {opt['package']}: {opt['tip']}")
|
||||
|
||||
# Next.js config
|
||||
if "nextjs" in analysis:
|
||||
nextjs = analysis["nextjs"]
|
||||
if nextjs.get("suggestions"):
|
||||
print("\n--- NEXT.JS CONFIG ---")
|
||||
for suggestion in nextjs["suggestions"]:
|
||||
print(f" - {suggestion}")
|
||||
|
||||
# Import issues
|
||||
if analysis.get("imports", {}).get("issues"):
|
||||
print("\n--- IMPORT ISSUES ---")
|
||||
for issue in analysis["imports"]["issues"][:10]: # Limit to 10
|
||||
print(f" - {issue['file']}: {issue['issue']}")
|
||||
|
||||
# Summary
|
||||
print("\n--- RECOMMENDATIONS ---")
|
||||
if score >= 90:
|
||||
print(" Bundle is well-optimized!")
|
||||
elif deps["issues"]:
|
||||
print(" 1. Replace heavy dependencies with lighter alternatives")
|
||||
if deps["warnings"]:
|
||||
print(" 2. Move dev-only packages to devDependencies")
|
||||
if deps["optimizations"]:
|
||||
print(" 3. Apply import optimizations for tree-shaking")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
class BundleAnalyzer:
|
||||
"""Main class for bundle analyzer functionality"""
|
||||
|
||||
def __init__(self, target_path: str, verbose: bool = False):
|
||||
self.target_path = Path(target_path)
|
||||
self.verbose = verbose
|
||||
self.results = {}
|
||||
|
||||
def run(self) -> Dict:
|
||||
"""Execute the main functionality"""
|
||||
print(f"🚀 Running {self.__class__.__name__}...")
|
||||
print(f"📁 Target: {self.target_path}")
|
||||
|
||||
try:
|
||||
self.validate_target()
|
||||
self.analyze()
|
||||
self.generate_report()
|
||||
|
||||
print("✅ Completed successfully!")
|
||||
return self.results
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def validate_target(self):
|
||||
"""Validate the target path exists and is accessible"""
|
||||
if not self.target_path.exists():
|
||||
raise ValueError(f"Target path does not exist: {self.target_path}")
|
||||
|
||||
if self.verbose:
|
||||
print(f"✓ Target validated: {self.target_path}")
|
||||
|
||||
def analyze(self):
|
||||
"""Perform the main analysis or operation"""
|
||||
if self.verbose:
|
||||
print("📊 Analyzing...")
|
||||
|
||||
# Main logic here
|
||||
self.results['status'] = 'success'
|
||||
self.results['target'] = str(self.target_path)
|
||||
self.results['findings'] = []
|
||||
|
||||
# Add analysis results
|
||||
if self.verbose:
|
||||
print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings")
|
||||
|
||||
def generate_report(self):
|
||||
"""Generate and display the report"""
|
||||
print("\n" + "="*50)
|
||||
print("REPORT")
|
||||
print("="*50)
|
||||
print(f"Target: {self.results.get('target')}")
|
||||
print(f"Status: {self.results.get('status')}")
|
||||
print(f"Findings: {len(self.results.get('findings', []))}")
|
||||
print("="*50 + "\n")
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Bundle Analyzer"
|
||||
description="Analyze frontend project for bundle optimization opportunities"
|
||||
)
|
||||
parser.add_argument(
|
||||
'target',
|
||||
help='Target path to analyze or process'
|
||||
"project_dir",
|
||||
nargs="?",
|
||||
default=".",
|
||||
help="Project directory to analyze (default: current directory)"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--verbose', '-v',
|
||||
action='store_true',
|
||||
help='Enable verbose output'
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Output in JSON format"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--json',
|
||||
action='store_true',
|
||||
help='Output results as JSON'
|
||||
"--verbose", "-v",
|
||||
action="store_true",
|
||||
help="Include detailed import analysis"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output', '-o',
|
||||
help='Output file path'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
tool = BundleAnalyzer(
|
||||
args.target,
|
||||
verbose=args.verbose
|
||||
)
|
||||
|
||||
results = tool.run()
|
||||
|
||||
if args.json:
|
||||
output = json.dumps(results, indent=2)
|
||||
if args.output:
|
||||
with open(args.output, 'w') as f:
|
||||
f.write(output)
|
||||
print(f"Results written to {args.output}")
|
||||
else:
|
||||
print(output)
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parser.parse_args()
|
||||
project_dir = Path(args.project_dir).resolve()
|
||||
|
||||
if not project_dir.exists():
|
||||
print(f"Error: Directory not found: {project_dir}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
package_json = load_package_json(project_dir)
|
||||
if not package_json:
|
||||
print("Error: No valid package.json found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
analysis = {
|
||||
"project": str(project_dir),
|
||||
"dependencies": analyze_dependencies(package_json),
|
||||
"nextjs": check_nextjs_config(project_dir)
|
||||
}
|
||||
|
||||
if args.verbose:
|
||||
analysis["imports"] = analyze_imports(project_dir)
|
||||
|
||||
analysis["score"], analysis["grade"] = calculate_score(analysis)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(analysis, indent=2))
|
||||
else:
|
||||
print_report(analysis)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,114 +1,329 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Component Generator
|
||||
Automated tool for senior frontend tasks
|
||||
React Component Generator
|
||||
|
||||
Generates React/Next.js component files with TypeScript, Tailwind CSS,
|
||||
and optional test files following best practices.
|
||||
|
||||
Usage:
|
||||
python component_generator.py Button --dir src/components/ui
|
||||
python component_generator.py ProductCard --type client --with-test
|
||||
python component_generator.py UserProfile --type server --with-story
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# Component templates
|
||||
TEMPLATES = {
|
||||
"client": '''\'use client\';
|
||||
|
||||
import {{ useState }} from 'react';
|
||||
import {{ cn }} from '@/lib/utils';
|
||||
|
||||
interface {name}Props {{
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}}
|
||||
|
||||
export function {name}({{ className, children }}: {name}Props) {{
|
||||
return (
|
||||
<div className={{cn('', className)}}>
|
||||
{{children}}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
''',
|
||||
|
||||
"server": '''import {{ cn }} from '@/lib/utils';
|
||||
|
||||
interface {name}Props {{
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}}
|
||||
|
||||
export async function {name}({{ className, children }}: {name}Props) {{
|
||||
return (
|
||||
<div className={{cn('', className)}}>
|
||||
{{children}}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
''',
|
||||
|
||||
"hook": '''import {{ useState, useEffect, useCallback }} from 'react';
|
||||
|
||||
interface Use{name}Options {{
|
||||
// Add options here
|
||||
}}
|
||||
|
||||
interface Use{name}Return {{
|
||||
// Add return type here
|
||||
isLoading: boolean;
|
||||
error: Error | null;
|
||||
}}
|
||||
|
||||
export function use{name}(options: Use{name}Options = {{}}): Use{name}Return {{
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {{
|
||||
// Effect logic here
|
||||
}}, []);
|
||||
|
||||
return {{
|
||||
isLoading,
|
||||
error,
|
||||
}};
|
||||
}}
|
||||
''',
|
||||
|
||||
"test": '''import {{ render, screen }} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {{ {name} }} from './{name}';
|
||||
|
||||
describe('{name}', () => {{
|
||||
it('renders correctly', () => {{
|
||||
render(<{name}>Test content</{name}>);
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument();
|
||||
}});
|
||||
|
||||
it('applies custom className', () => {{
|
||||
render(<{name} className="custom-class">Content</{name}>);
|
||||
expect(screen.getByText('Content').parentElement).toHaveClass('custom-class');
|
||||
}});
|
||||
|
||||
// Add more tests here
|
||||
}});
|
||||
''',
|
||||
|
||||
"story": '''import type {{ Meta, StoryObj }} from '@storybook/react';
|
||||
import {{ {name} }} from './{name}';
|
||||
|
||||
const meta: Meta<typeof {name}> = {{
|
||||
title: 'Components/{name}',
|
||||
component: {name},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {{
|
||||
className: {{
|
||||
control: 'text',
|
||||
description: 'Additional CSS classes',
|
||||
}},
|
||||
}},
|
||||
}};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof {name}>;
|
||||
|
||||
export const Default: Story = {{
|
||||
args: {{
|
||||
children: 'Default content',
|
||||
}},
|
||||
}};
|
||||
|
||||
export const WithCustomClass: Story = {{
|
||||
args: {{
|
||||
className: 'bg-blue-100 p-4',
|
||||
children: 'Styled content',
|
||||
}},
|
||||
}};
|
||||
''',
|
||||
|
||||
"index": '''export {{ {name} }} from './{name}';
|
||||
export type {{ {name}Props }} from './{name}';
|
||||
''',
|
||||
}
|
||||
|
||||
|
||||
def to_pascal_case(name: str) -> str:
|
||||
"""Convert string to PascalCase."""
|
||||
# Handle kebab-case and snake_case
|
||||
words = name.replace('-', '_').split('_')
|
||||
return ''.join(word.capitalize() for word in words)
|
||||
|
||||
|
||||
def to_kebab_case(name: str) -> str:
|
||||
"""Convert PascalCase to kebab-case."""
|
||||
result = []
|
||||
for i, char in enumerate(name):
|
||||
if char.isupper() and i > 0:
|
||||
result.append('-')
|
||||
result.append(char.lower())
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
def generate_component(
|
||||
name: str,
|
||||
output_dir: Path,
|
||||
component_type: str = "client",
|
||||
with_test: bool = False,
|
||||
with_story: bool = False,
|
||||
with_index: bool = True,
|
||||
flat: bool = False,
|
||||
) -> dict:
|
||||
"""Generate component files."""
|
||||
pascal_name = to_pascal_case(name)
|
||||
kebab_name = to_kebab_case(pascal_name)
|
||||
|
||||
# Determine output path
|
||||
if flat:
|
||||
component_dir = output_dir
|
||||
else:
|
||||
component_dir = output_dir / pascal_name
|
||||
|
||||
files_created = []
|
||||
|
||||
# Create directory
|
||||
component_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Generate main component file
|
||||
if component_type == "hook":
|
||||
main_file = component_dir / f"use{pascal_name}.ts"
|
||||
template = TEMPLATES["hook"]
|
||||
else:
|
||||
main_file = component_dir / f"{pascal_name}.tsx"
|
||||
template = TEMPLATES[component_type]
|
||||
|
||||
content = template.format(name=pascal_name)
|
||||
main_file.write_text(content)
|
||||
files_created.append(str(main_file))
|
||||
|
||||
# Generate test file
|
||||
if with_test and component_type != "hook":
|
||||
test_file = component_dir / f"{pascal_name}.test.tsx"
|
||||
test_content = TEMPLATES["test"].format(name=pascal_name)
|
||||
test_file.write_text(test_content)
|
||||
files_created.append(str(test_file))
|
||||
|
||||
# Generate story file
|
||||
if with_story and component_type != "hook":
|
||||
story_file = component_dir / f"{pascal_name}.stories.tsx"
|
||||
story_content = TEMPLATES["story"].format(name=pascal_name)
|
||||
story_file.write_text(story_content)
|
||||
files_created.append(str(story_file))
|
||||
|
||||
# Generate index file
|
||||
if with_index and not flat:
|
||||
index_file = component_dir / "index.ts"
|
||||
index_content = TEMPLATES["index"].format(name=pascal_name)
|
||||
index_file.write_text(index_content)
|
||||
files_created.append(str(index_file))
|
||||
|
||||
return {
|
||||
"name": pascal_name,
|
||||
"type": component_type,
|
||||
"directory": str(component_dir),
|
||||
"files": files_created,
|
||||
}
|
||||
|
||||
|
||||
def print_result(result: dict, verbose: bool = False) -> None:
|
||||
"""Print generation result."""
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Component Generated: {result['name']}")
|
||||
print(f"{'='*50}")
|
||||
print(f"Type: {result['type']}")
|
||||
print(f"Directory: {result['directory']}")
|
||||
print(f"\nFiles created:")
|
||||
for file in result['files']:
|
||||
print(f" - {file}")
|
||||
print(f"{'='*50}\n")
|
||||
|
||||
# Print usage hint
|
||||
if result['type'] != 'hook':
|
||||
print("Usage:")
|
||||
print(f" import {{ {result['name']} }} from '@/components/{result['name']}';")
|
||||
print(f"\n <{result['name']}>Content</{result['name']}>")
|
||||
else:
|
||||
print("Usage:")
|
||||
print(f" import {{ use{result['name']} }} from '@/hooks/use{result['name']}';")
|
||||
print(f"\n const {{ isLoading, error }} = use{result['name']}();")
|
||||
|
||||
class ComponentGenerator:
|
||||
"""Main class for component generator functionality"""
|
||||
|
||||
def __init__(self, target_path: str, verbose: bool = False):
|
||||
self.target_path = Path(target_path)
|
||||
self.verbose = verbose
|
||||
self.results = {}
|
||||
|
||||
def run(self) -> Dict:
|
||||
"""Execute the main functionality"""
|
||||
print(f"🚀 Running {self.__class__.__name__}...")
|
||||
print(f"📁 Target: {self.target_path}")
|
||||
|
||||
try:
|
||||
self.validate_target()
|
||||
self.analyze()
|
||||
self.generate_report()
|
||||
|
||||
print("✅ Completed successfully!")
|
||||
return self.results
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def validate_target(self):
|
||||
"""Validate the target path exists and is accessible"""
|
||||
if not self.target_path.exists():
|
||||
raise ValueError(f"Target path does not exist: {self.target_path}")
|
||||
|
||||
if self.verbose:
|
||||
print(f"✓ Target validated: {self.target_path}")
|
||||
|
||||
def analyze(self):
|
||||
"""Perform the main analysis or operation"""
|
||||
if self.verbose:
|
||||
print("📊 Analyzing...")
|
||||
|
||||
# Main logic here
|
||||
self.results['status'] = 'success'
|
||||
self.results['target'] = str(self.target_path)
|
||||
self.results['findings'] = []
|
||||
|
||||
# Add analysis results
|
||||
if self.verbose:
|
||||
print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings")
|
||||
|
||||
def generate_report(self):
|
||||
"""Generate and display the report"""
|
||||
print("\n" + "="*50)
|
||||
print("REPORT")
|
||||
print("="*50)
|
||||
print(f"Target: {self.results.get('target')}")
|
||||
print(f"Status: {self.results.get('status')}")
|
||||
print(f"Findings: {len(self.results.get('findings', []))}")
|
||||
print("="*50 + "\n")
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Component Generator"
|
||||
description="Generate React/Next.js components with TypeScript and Tailwind CSS"
|
||||
)
|
||||
parser.add_argument(
|
||||
'target',
|
||||
help='Target path to analyze or process'
|
||||
"name",
|
||||
help="Component name (PascalCase or kebab-case)"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--verbose', '-v',
|
||||
action='store_true',
|
||||
help='Enable verbose output'
|
||||
"--dir", "-d",
|
||||
default="src/components",
|
||||
help="Output directory (default: src/components)"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--json',
|
||||
action='store_true',
|
||||
help='Output results as JSON'
|
||||
"--type", "-t",
|
||||
choices=["client", "server", "hook"],
|
||||
default="client",
|
||||
help="Component type (default: client)"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output', '-o',
|
||||
help='Output file path'
|
||||
"--with-test",
|
||||
action="store_true",
|
||||
help="Generate test file"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
tool = ComponentGenerator(
|
||||
args.target,
|
||||
verbose=args.verbose
|
||||
parser.add_argument(
|
||||
"--with-story",
|
||||
action="store_true",
|
||||
help="Generate Storybook story file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-index",
|
||||
action="store_true",
|
||||
help="Skip generating index.ts file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--flat",
|
||||
action="store_true",
|
||||
help="Create files directly in output dir without subdirectory"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Show what would be generated without creating files"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verbose", "-v",
|
||||
action="store_true",
|
||||
help="Enable verbose output"
|
||||
)
|
||||
|
||||
results = tool.run()
|
||||
|
||||
if args.json:
|
||||
output = json.dumps(results, indent=2)
|
||||
if args.output:
|
||||
with open(args.output, 'w') as f:
|
||||
f.write(output)
|
||||
print(f"Results written to {args.output}")
|
||||
else:
|
||||
print(output)
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parser.parse_args()
|
||||
|
||||
output_dir = Path(args.dir)
|
||||
pascal_name = to_pascal_case(args.name)
|
||||
|
||||
if args.dry_run:
|
||||
print(f"\nDry run - would generate:")
|
||||
print(f" Component: {pascal_name}")
|
||||
print(f" Type: {args.type}")
|
||||
print(f" Directory: {output_dir / pascal_name if not args.flat else output_dir}")
|
||||
print(f" Test: {'Yes' if args.with_test else 'No'}")
|
||||
print(f" Story: {'Yes' if args.with_story else 'No'}")
|
||||
return
|
||||
|
||||
try:
|
||||
result = generate_component(
|
||||
name=args.name,
|
||||
output_dir=output_dir,
|
||||
component_type=args.type,
|
||||
with_test=args.with_test,
|
||||
with_story=args.with_story,
|
||||
with_index=not args.no_index,
|
||||
flat=args.flat,
|
||||
)
|
||||
print_result(result, args.verbose)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user