diff --git a/engineering-team/senior-frontend/SKILL.md b/engineering-team/senior-frontend/SKILL.md index 714c1b8..6c9c592 100644 --- a/engineering-team/senior-frontend/SKILL.md +++ b/engineering-team/senior-frontend/SKILL.md @@ -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 [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 ( +
+ {children} +
+ ); +} ``` -### 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 [--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 ( + + {children} + + ); +}; + +Tabs.List = TabList; +Tabs.Panel = TabPanel; + +// Usage + + + One + Two + + Content 1 + Content 2 + ``` -### 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(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 + + loading ? : + } +/> ``` -### 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 ( +
+

{product.name}

+ {/* Client component */} +
+ ); +} + +// Client Component +'use client'; +function AddToCartButton({ productId }) { + const [adding, setAdding] = useState(false); + return ; +} ``` -### 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 +Hero -### 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 +
+ Product +
``` -## Troubleshooting +### Data Fetching Patterns -### Common Issues +```tsx +// Parallel fetching +async function Dashboard() { + const [user, stats] = await Promise.all([ + getUser(), + getStats() + ]); + return
...
; +} -Check the comprehensive troubleshooting section in `references/frontend_best_practices.md`. +// Streaming with Suspense +async function ProductPage({ params }) { + return ( +
+ + }> + + +
+ ); +} +``` -### 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 (` + +// Skip link for keyboard users + + Skip to main content + +``` + +### 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(); + + await userEvent.click(screen.getByRole('button')); + expect(onClick).toHaveBeenCalledTimes(1); +}); + +// Test accessibility +test('dialog is accessible', async () => { + render(); + + 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'; + + +
...
+ +
...
+
...
+ +
...
+``` + +### Keyboard Navigation + +```tsx +// Ensure all interactive elements are keyboard accessible +function Modal({ isOpen, onClose, children }: ModalProps) { + const modalRef = useRef(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 ( +
+ {children} +
+ ); } ``` -**Benefits:** -- Benefit 1 -- Benefit 2 -- Benefit 3 +### ARIA Attributes -**Trade-offs:** -- Consider 1 -- Consider 2 -- Consider 3 +```tsx +// Live regions for dynamic content +
+ {status &&

{status}

} +
-### Pattern 2: Advanced Technique +// Loading states + -**Description:** -Another important pattern for senior frontend. +// Form labels + + +{errors.email && ( + +)} + +// Navigation + + +// Toggle buttons + + +// Expandable sections + + +``` + +### 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 + + +``` + +### 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 + + Skip to main content + + +// Icon buttons need labels + + +// Or use visually hidden text + +``` + +--- + +## 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(); + expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument(); + }); + + it('calls onClick when clicked', async () => { + const user = userEvent.setup(); + const handleClick = jest.fn(); + + render(); + await user.click(screen.getByRole('button')); + + expect(handleClick).toHaveBeenCalledTimes(1); + }); + + it('is disabled when loading', () => { + render(); + expect(screen.getByRole('button')).toBeDisabled(); + expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true'); + }); + + it('shows loading text when loading', () => { + render(); + 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( + + + + ); + + 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( + + + + ); + + 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 { + variant?: 'primary' | 'secondary'; + isLoading?: boolean; +} + +function Button({ variant = 'primary', isLoading, children, ...props }: ButtonProps) { + return ( + + ); +} + +// Polymorphic components +type PolymorphicProps = { + as?: E; +} & React.ComponentPropsWithoutRef; + +function Box({ + as, + children, + ...props +}: PolymorphicProps) { + const Component = as || 'div'; + return {children}; +} + +// Usage +Content +Article content +``` + +### Discriminated Unions + +```tsx +// State machines with exhaustive type checking +type AsyncState = + | { status: 'idle' } + | { status: 'loading' } + | { status: 'success'; data: T } + | { status: 'error'; error: Error }; + +function DataDisplay({ state, render }: { + state: AsyncState; + render: (data: T) => React.ReactNode; +}) { + switch (state.status) { + case 'idle': + return null; + case 'loading': + return ; + case 'success': + return <>{render(state.data)}; + case 'error': + return ; + // 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 { + 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({ items, renderItem, keyExtractor, emptyMessage }: ListProps) { + if (items.length === 0) { + return

{emptyMessage || 'No items'}

; + } -### Security Best Practices -- Input validation -- Authentication -- Authorization -- Data protection + return ( +
    + {items.map((item, index) => ( +
  • {renderItem(item, index)}
  • + ))} +
+ ); +} -## Common Patterns +// Usage + user.id} + renderItem={(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 Admin ({user.permissions.length} perms); + } -### Anti-Pattern 2 -What not to do and why. + return User; +} -## Tools and Resources +// API response type guards +interface ApiSuccess { + 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 = ApiSuccess | ApiError; -## Conclusion +function isApiSuccess(response: ApiResponse): response is ApiSuccess { + 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, + VariantProps {} + +function Button({ className, variant, size, ...props }: ButtonProps) { + return ( + + +``` + +### Responsive Design + +```tsx +// Mobile-first responsive design +
+ {products.map(product => )} +
+ +// Container with responsive padding +
+ Content +
+ +// Hide/show based on breakpoint + + +``` + +### Animation Utilities + +```tsx +// Skeleton loading +
+
+
+
+ +// Transitions + + +// 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 +
Fading in
+``` + +--- + +## 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 +
{userInput}
+ +// 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
; +} +``` + +### 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; + +function RegisterForm() { + const { register, handleSubmit, formState: { errors } } = useForm({ + resolver: zodResolver(schema), + }); + + return ( +
+ + + + +
+ ); +} +``` + +### 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()); +} +``` diff --git a/engineering-team/senior-frontend/references/nextjs_optimization_guide.md b/engineering-team/senior-frontend/references/nextjs_optimization_guide.md index 16e07cb..d1157a3 100644 --- a/engineering-team/senior-frontend/references/nextjs_optimization_guide.md +++ b/engineering-team/senior-frontend/references/nextjs_optimization_guide.md @@ -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 ( +
+ {products.map(product => ( + + ))} +
+ ); } ``` -**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 ( + + ); } ``` -## 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 ( +
+ {/* Server-rendered content */} +

{product.name}

+

{product.description}

-### Security Best Practices -- Input validation -- Authentication -- Authorization -- Data protection + {/* Client component for interactivity */} + -## Common Patterns + {/* Server component for reviews */} + +
+ ); +} +``` -### 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 +Hero image + +// Responsive image +Product + +// With placeholder blur +import productImage from '@/public/product.jpg'; + +Product +``` + +### 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) +Gallery photo + +// Above the fold - load immediately +Hero +``` + +--- + +## Code Splitting + +### Dynamic Imports + +```tsx +import dynamic from 'next/dynamic'; + +// Basic dynamic import +const HeavyChart = dynamic(() => import('@/components/HeavyChart'), { + loading: () => , +}); + +// Disable SSR for client-only components +const MapComponent = dynamic(() => import('@/components/Map'), { + ssr: false, + loading: () =>
, +}); + +// Named exports +const Modal = dynamic(() => + import('@/components/ui').then(mod => mod.Modal) +); + +// With suspense +const DashboardCharts = dynamic(() => import('@/components/DashboardCharts'), { + loading: () => } />, +}); +``` + +### 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 ( + }> + + + ); +} +``` + +### 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 ( +
+ {children} + }>{analytics} + }>{metrics} +
+ ); +} +``` + +--- + +## 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 ( +
+ + + +
+ ); +} +``` + +### Streaming with Suspense + +```tsx +import { Suspense } from 'react'; + +async function ProductPage({ params }: { params: { id: string } }) { + const product = await getProduct(params.id); + + return ( +
+ {/* Immediate content */} +

{product.name}

+

{product.description}

+ + {/* Stream reviews - don't block page */} + }> + + + + {/* Stream recommendations */} + }> + + +
+ ); +} + +// Slow data component +async function Reviews({ productId }: { productId: string }) { + const reviews = await getReviews(productId); // Slow query + return ; +} +``` + +### Request Memoization + +```tsx +// Next.js automatically dedupes identical requests +async function Layout({ children }) { + const user = await getUser(); // Request 1 + return
{children}
; +} + +async function Header() { + const user = await getUser(); // Same request - cached! + return
Hello, {user.name}
; +} + +// 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 ( + + {children} + + ); +} +``` + +--- + +## Core Web Vitals + +### Largest Contentful Paint (LCP) + +```tsx +// Optimize LCP hero image +import Image from 'next/image'; + +export default function Hero() { + return ( +
+ Hero +
+

Welcome

+
+
+ ); +} + +// Preload critical resources in layout +export default function RootLayout({ children }) { + return ( + + + + + + {children} + + ); +} +``` + +### Cumulative Layout Shift (CLS) + +```tsx +// Prevent CLS with explicit dimensions +Product + +// Or use aspect ratio +
+ Video +
+ +// Skeleton placeholders +function ProductCard({ product }: { product?: Product }) { + if (!product) { + return ( +
+
+
+
+
+ ); + } + + return ( +
+ {product.name} +

{product.name}

+

{product.price}

+
+ ); +} +``` + +### 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 ( + + + {children} + + {/* Load analytics after page is interactive */} + + + +''', + } + + +def scaffold_project( + name: str, + output_dir: Path, + template: str = "nextjs", + features: Optional[List[str]] = None, + dry_run: bool = False, +) -> Dict: + """Scaffold a complete frontend project.""" + features = features or [] + project_path = output_dir / name + + if project_path.exists() and not dry_run: + return {"error": f"Directory already exists: {project_path}"} + + template_config = TEMPLATES.get(template) + if not template_config: + return {"error": f"Unknown template: {template}"} + + created_files = [] + + # Create project directory + if not dry_run: + project_path.mkdir(parents=True, exist_ok=True) + + # Generate base structure + created_files.extend( + generate_structure(project_path, template_config["structure"], dry_run) + ) + + # Generate config files + created_files.extend( + generate_config_files(project_path, template, name, features, dry_run) + ) + + # Add feature files + for feature in features: + if feature in FEATURES: + for file_path, content_key in FEATURES[feature]["files"].items(): + full_path = project_path / file_path + if not dry_run: + full_path.parent.mkdir(parents=True, exist_ok=True) + content = FILE_CONTENTS.get(content_key, f"// TODO: Implement {content_key}") + full_path.write_text(content) + created_files.append(str(full_path)) + + return { + "name": name, + "template": template, + "template_name": template_config["name"], + "features": features, + "path": str(project_path), + "files_created": len(created_files), + "files": created_files, + "next_steps": [ + f"cd {name}", + "npm install", + "npm run dev", + ], + } + + +def print_result(result: Dict) -> None: + """Print scaffolding result.""" + if "error" in result: + print(f"Error: {result['error']}", file=sys.stderr) + return + + print(f"\n{'='*60}") + print(f"Project Scaffolded: {result['name']}") + print(f"{'='*60}") + print(f"Template: {result['template_name']}") + print(f"Location: {result['path']}") + print(f"Files Created: {result['files_created']}") + + if result["features"]: + print(f"Features: {', '.join(result['features'])}") + + print(f"\nNext Steps:") + for step in result["next_steps"]: + print(f" $ {step}") + + print(f"{'='*60}\n") + def main(): - """Main entry point""" parser = argparse.ArgumentParser( - description="Frontend Scaffolder" + description="Scaffold a frontend project with best practices" ) parser.add_argument( - 'target', - help='Target path to analyze or process' + "name", + help="Project name (kebab-case recommended)" ) parser.add_argument( - '--verbose', '-v', - action='store_true', - help='Enable verbose output' + "--dir", "-d", + default=".", + help="Output directory (default: current directory)" ) parser.add_argument( - '--json', - action='store_true', - help='Output results as JSON' + "--template", "-t", + choices=list(TEMPLATES.keys()), + default="nextjs", + help="Project template (default: nextjs)" ) parser.add_argument( - '--output', '-o', - help='Output file path' + "--features", "-f", + help="Comma-separated features to add (auth,api,forms,testing,storybook)" ) - - args = parser.parse_args() - - tool = FrontendScaffolder( - args.target, - verbose=args.verbose + parser.add_argument( + "--list-templates", + action="store_true", + help="List available templates" + ) + parser.add_argument( + "--list-features", + action="store_true", + help="List available features" + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would be created without creating files" + ) + parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format" ) - - 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() + + if args.list_templates: + print("\nAvailable Templates:") + for key, template in TEMPLATES.items(): + print(f" {key}: {template['name']}") + print(f" {template['description']}") + return + + if args.list_features: + print("\nAvailable Features:") + for key, feature in FEATURES.items(): + print(f" {key}: {feature['description']}") + deps = ", ".join(feature.get("dependencies", [])) + if deps: + print(f" Adds: {deps}") + return + + features = [] + if args.features: + features = [f.strip() for f in args.features.split(",")] + invalid = [f for f in features if f not in FEATURES] + if invalid: + print(f"Unknown features: {', '.join(invalid)}", file=sys.stderr) + print(f"Valid features: {', '.join(FEATURES.keys())}") + sys.exit(1) + + result = scaffold_project( + name=args.name, + output_dir=Path(args.dir), + template=args.template, + features=features, + dry_run=args.dry_run, + ) + + if args.json: + print(json.dumps(result, indent=2)) + else: + print_result(result) + + +if __name__ == "__main__": main()