fix(skill): rewrite senior-frontend with React/Next.js content (#63) (#118)

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:
Alireza Rezvani
2026-01-30 03:40:48 +01:00
committed by GitHub
parent fe0c5d4565
commit 829a197c2b
7 changed files with 4272 additions and 642 deletions

View File

@@ -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`

View File

@@ -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());
}
```

View File

@@ -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;
```

View File

@@ -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]
);
```

View File

@@ -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()

View File

@@ -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