senior-frontend
Frontend development for React 19, Next.js 15, TypeScript, and Tailwind CSS v4 / Pengembangan frontend dengan React 19, Next.js 15, TypeScript, dan Tailwind CSS v4.
Install
npx skills add https://github.com/roedyrustam/vibes-plug/tree/main/skills/senior-frontend
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install roedyrustam-vibes-plug@llmmart
git clone https://github.com/roedyrustam/vibes-plug.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole roedyrustam/vibes-plug collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Senior Frontend Specialist (2026 Edition)
English
Description
Production-grade frontend development patterns, performance optimization, and modern ecosystem integrations for React 19 / Next.js 15 applications with Tailwind CSS v4 and TypeScript. Covers React 19 Compiler, Partial Prerendering (PPR), View Transitions API, and SPA/MPA hybrid strategies with automated self-healing testing loops.
Orchestration & Integration
design-system-architect: For building robust UI components (Radix, Base UI, shadcn) and design tokens.tailwind-expert: For styling and custom @theme configurations.svg-animation-motion-expert: For advanced animations, GSAP timelines, and motion physics.nextjs-app-router-expert: For RSC, caching, and Next.js routing patterns.state-management-expert: For client-side state (Zustand, Jotai).tanstack-query-expert: For advanced data fetching and server state caching.spa-orchestrator: For Single-Page Application architectures.mpa-orchestrator: For Multi-Page Application architectures.autonomous-tdd-debugger: For automated unit/component test execution and stack-trace self-healing.
Trigger Conditions
- Scaffold a new React or Next.js 15 project with TypeScript and Tailwind CSS v4.
- Generate new components, custom hooks, or Server Actions.
- Analyze and optimize bundle sizes and Core Web Vitals.
- Implement advanced React 19 patterns (
useActionState,useOptimistic,use()). - Build SPAs — coordinate with
spa-orchestrator. - Ensure accessibility compliance (WCAG 2.2) and testing.
Technical Guidelines & Best Practices
1. Next.js 15: Server vs Client Components
Use Server Components by default for performance and SEO. Use 'use client' strictly for:
- State (
useState,useEffect), event handlers (onClick), browser APIs. - Interactive UI needing client hydration.
Next.js 15.2+ Requirements:
- Await
Promise-basedparamsandsearchParamsbefore accessing properties. - Use Partial Prerendering (PPR) natively. Wrap dynamic sections in
<Suspense>.
2. React 19 Compiler & Hooks
Do not use useMemo, useCallback, or React.memo unless explicitly required; rely on the React 19 compiler.
Mandatory Hooks:
useActionState: Manage form state, pending indicators, and action results with Server Actions.useOptimistic: Update UI instantly before server confirmation.useFormStatus: Read form submission state in child components.use(promise): Unwrap promises or context inside render with Suspense.
3. View Transitions API
Implement native View Transitions API for smooth page transitions. Use Next.js 15 <Link viewTransition> natively.
4. Tailwind CSS v4
Use CSS-first configuration. Define custom tokens with @theme and register plugins with @plugin in the main CSS file. Never create tailwind.config.js.
5. Accessibility (WCAG 2.2) & Self-Healing Testing
- Enforce semantic HTML (
<button>,<nav>,<main>,<article>). - Enforce full keyboard navigability and valid
aria-*labels. - Run typecheck (
tsc --noEmit) to verify zero compiler diagnostics. - Write unit tests using Vitest and React Testing Library.
- When tests fail, activate
autonomous-tdd-debuggerto self-heal until 100% passing.
6. Architecture Integration
- SPA (
spa-orchestrator): Use TanStack Router for type-safe client routing and TanStack Query v5 for server state. Never use bareuseEffectfor data fetching. - MPA (
mpa-orchestrator): Use Alpine.js or HTMX for micro-interactions. Apply Progressive Enhancement.
7. Animations & Motion
For web animations, scroll-driven timelines, GSAP choreographies, and Framer Motion spring physics, delegate directly to svg-animation-motion-expert.
Bahasa Indonesia
Deskripsi
Pola pengembangan frontend tingkat produksi, optimasi performa, dan integrasi ekosistem modern untuk aplikasi React 19 / Next.js 15 dengan Tailwind CSS v4 dan TypeScript. Mencakup React 19 Compiler, Partial Prerendering (PPR), View Transitions API, dan strategi hybrid SPA/MPA dengan loop pengujian perbaikan mandiri (self-healing).
Integrasi Orkestrasi
design-system-architect: Untuk membangun komponen UI yang kuat (Radix, Base UI, shadcn) dan design token.tailwind-expert: Untuk styling dan konfigurasi @theme kustom.svg-animation-motion-expert: Untuk animasi web, timeline GSAP, dan fisika spring.nextjs-app-router-expert: Untuk RSC, caching, dan pola routing Next.js.state-management-expert: Untuk state client-side (Zustand, Jotai).tanstack-query-expert: Untuk fetching data lanjutan dan caching server state.spa-orchestrator: Untuk arsitektur Single-Page Application.mpa-orchestrator: Untuk arsitektur Multi-Page Application.autonomous-tdd-debugger: Untuk eksekusi pengujian otomatis dan perbaikan mandiri berdasarkan stack trace terminal.
Kondisi Pemicu
- Buat proyek React atau Next.js 15 baru dengan TypeScript dan Tailwind CSS v4.
- Buat komponen baru, custom hooks, atau Server Actions.
- Analisis dan optimalkan ukuran bundle dan Core Web Vitals.
- Terapkan pola React 19 lanjutan (
useActionState,useOptimistic,use()). - Bangun SPA — koordinasikan dengan
spa-orchestrator. - Pastikan kepatuhan aksesibilitas (WCAG 2.2) dan pengujian.
Panduan Teknis & Praktik Terbaik
1. Next.js 15: Server vs Client Components
Gunakan Server Components secara default. Gunakan 'use client' hanya untuk:
- State (
useState,useEffect), event handler, API browser. - UI interaktif yang membutuhkan hidrasi klien.
Persyaratan Next.js 15.2+:
- Wajib
awaitpadaparamsdansearchParamsyang kini bertipePromise. - Gunakan Partial Prerendering (PPR) secara native. Bungkus bagian dinamis dengan
<Suspense>.
2. React 19 Compiler & Hook Baru
Jangan gunakan useMemo, useCallback, atau React.memo kecuali sangat diperlukan; andalkan compiler React 19.
Hook Wajib:
useActionState: Kelola state form, indikator loading, dan hasil action dengan Server Actions.useOptimistic: Perbarui UI secara instan sebelum server mengonfirmasi.useFormStatus: Baca status form di dalam komponen anak.use(promise): Buka promise atau context langsung di dalam render dengan Suspense.
3. View Transitions API
Terapkan View Transitions API bawaan browser untuk transisi halaman. Gunakan <Link viewTransition> pada Next.js 15.
4. Tailwind CSS v4
Gunakan konfigurasi CSS-first. Definisikan token kustom dengan @theme dan plugin dengan @plugin di file CSS utama (lihat tailwind-expert). Jangan pernah membuat file tailwind.config.js.
5. Aksesibilitas (WCAG 2.2) & Pengujian Self-Healing
- Wajib gunakan HTML semantik (
<button>,<nav>,<main>). - Wajib pastikan navigasi keyboard dengan label
aria-*yang valid. - Jalankan pemeriksaan tipe (
tsc --noEmit) untuk memastikan nol eror diagnostik. - Tulis pengujian unit dengan Vitest dan React Testing Library.
- Jika pengujian gagal, aktifkan
autonomous-tdd-debuggeruntuk memulihkan kode hingga 100% lolos.
6. Integrasi Arsitektur
- SPA (
spa-orchestrator): Gunakan TanStack Router untuk routing klien dan TanStack Query v5 untuk server state. Jangan gunakanuseEffectmurni untuk fetching data. - MPA (
mpa-orchestrator): Gunakan Alpine.js atau HTMX untuk interaksi mikro. Terapkan Progressive Enhancement.
7. Animasi & Motion
Untuk animasi web, timeline scroll-driven, koreografi GSAP, dan fisika spring Framer Motion, delegasikan langsung ke svg-animation-motion-expert.
Files (vibes-plug)
-
references
-
frontend_best_practices.md 19.3 KB
# Frontend Best Practices Modern frontend development standards for accessibility, testing, TypeScript, and Tailwind CSS. --- ## Table of Contents - [Accessibility (a11y)](#accessibility-a11y) - [Testing Strategies](#testing-strategies) - [TypeScript Patterns](#typescript-patterns) - [Tailwind CSS](#tailwind-css) - [Project Structure](#project-structure) - [Security](#security) --- ## Accessibility (a11y) ### 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> ); } ``` ### ARIA Attributes ```tsx // Live regions for dynamic content <div aria-live="polite" aria-atomic="true"> {status && <p>{status}</p>} </div> // Loading states <button disabled={isLoading} aria-busy={isLoading}> {isLoading ? 'Loading...' : 'Submit'} </button> // 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 ```typescript // 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 } } ``` ### Generic Components ```tsx // Generic list component interface ListProps<T> { items: T[]; renderItem: (item: T, index: number) => React.ReactNode; keyExtractor: (item: T) => string; emptyMessage?: string; } function List<T>({ items, renderItem, keyExtractor, emptyMessage }: ListProps<T>) { if (items.length === 0) { return <p className="text-muted">{emptyMessage || 'No items'}</p>; } return ( <ul> {items.map((item, index) => ( <li key={keyExtractor(item)}>{renderItem(item, index)}</li> ))} </ul> ); } // Usage <List items={users} keyExtractor={(user) => user.id} renderItem={(user) => <UserCard user={user} />} /> ``` ### Type Guards ```tsx // User-defined type guards interface User { id: string; name: string; email: string; } interface Admin extends User { role: 'admin'; permissions: string[]; } function isAdmin(user: User): user is Admin { return 'role' in user && user.role === 'admin'; } function UserBadge({ user }: { user: User }) { if (isAdmin(user)) { // TypeScript knows user is Admin here return <Badge variant="admin">Admin ({user.permissions.length} perms)</Badge>; } return <Badge>User</Badge>; } // API response type guards interface ApiSuccess<T> { success: true; data: T; } interface ApiError { success: false; error: string; } type ApiResponse<T> = ApiSuccess<T> | ApiError; function isApiSuccess<T>(response: ApiResponse<T>): response is ApiSuccess<T> { return response.success === true; } ``` --- ## 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()); } ``` -
nextjs_optimization_guide.md 14.8 KB
# Next.js Optimization Guide Performance optimization techniques for Next.js 15 applications with React Server Components, Turbopack, and modern caching strategies. --- ## Table of Contents - [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) --- ## Rendering Strategies ### 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> ); } ``` ### Client Components Use `'use client'` only when you need: - Event handlers (onClick, onChange) - State (useState, useReducer) - Effects (useEffect) - Browser APIs (window, document) ```tsx 'use client'; import { useState } from 'react'; 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> ); } ``` ### Mixing Server and Client Components ```tsx // app/products/[id]/page.tsx - Server Component async function ProductPage({ params }: { params: { id: string } }) { const product = await getProduct(params.id); return ( <div> {/* Server-rendered content */} <h1>{product.name}</h1> <p>{product.description}</p> {/* Client component for interactivity */} <AddToCartButton productId={product.id} /> {/* Server component for reviews */} <ProductReviews productId={product.id} /> </div> ); } ``` ### Static vs Dynamic Rendering ```tsx // Force static generation at build time export const dynamic = 'force-static'; // Force dynamic rendering at request time export const dynamic = 'force-dynamic'; // Revalidate every 60 seconds (ISR) export const revalidate = 60; // Revalidate on-demand import { revalidatePath, revalidateTag } from 'next/cache'; async function updateProduct(id: string, data: ProductData) { await db.products.update({ where: { id }, data }); // Revalidate specific path revalidatePath(`/products/${id}`); // Or revalidate by tag revalidateTag('products'); } ``` --- ## Image Optimization ### 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; ``` -
react_patterns.md 16.9 KB
# React Patterns Production-ready patterns for building scalable React applications with TypeScript. --- ## Table of Contents - [Component Composition](#component-composition) - [Custom Hooks](#custom-hooks) - [State Management](#state-management) - [Performance Patterns](#performance-patterns) - [Error Boundaries](#error-boundaries) - [Anti-Patterns](#anti-patterns) --- ## Component Composition ### 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} />; } ``` ### useDebounce - Debounce values ```tsx function useDebounce<T>(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timer = setTimeout(() => setDebouncedValue(value), delay); return () => clearTimeout(timer); }, [value, delay]); return debouncedValue; } // 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)} />; } ``` ### useLocalStorage - Persist state ```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; } }); 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]); return [storedValue, setValue] as const; } // Usage const [theme, setTheme] = useLocalStorage('theme', 'light'); ``` ### useMediaQuery - Responsive design ```tsx function useMediaQuery(query: string): boolean { const [matches, setMatches] = useState(false); useEffect(() => { const media = window.matchMedia(query); setMatches(media.matches); const listener = (e: MediaQueryListEvent) => setMatches(e.matches); media.addEventListener('change', listener); return () => media.removeEventListener('change', listener); }, [query]); return matches; } // Usage function ResponsiveNav() { const isMobile = useMediaQuery('(max-width: 768px)'); return isMobile ? <MobileNav /> : <DesktopNav />; } ``` ### usePrevious - Track previous values ```tsx function usePrevious<T>(value: T): T | undefined { const ref = useRef<T>(); useEffect(() => { ref.current = value; }, [value]); return ref.current; } // 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] ); ```
-
-
scripts
-
bundle_analyzer.py 13 KB
#!/usr/bin/env python3 """ 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 argparse import json import os import re import sys from pathlib import Path 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) def main(): parser = argparse.ArgumentParser( description="Analyze frontend project for bundle optimization opportunities" ) parser.add_argument( "project_dir", nargs="?", default=".", help="Project directory to analyze (default: current directory)" ) parser.add_argument( "--json", action="store_true", help="Output in JSON format" ) parser.add_argument( "--verbose", "-v", action="store_true", help="Include detailed import analysis" ) 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() -
component_generator.py 8.5 KB
#!/usr/bin/env python3 """ 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 from pathlib import Path 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']}();") def main(): parser = argparse.ArgumentParser( description="Generate React/Next.js components with TypeScript and Tailwind CSS" ) parser.add_argument( "name", help="Component name (PascalCase or kebab-case)" ) parser.add_argument( "--dir", "-d", default="src/components", help="Output directory (default: src/components)" ) parser.add_argument( "--type", "-t", choices=["client", "server", "hook"], default="client", help="Component type (default: client)" ) parser.add_argument( "--with-test", action="store_true", help="Generate test file" ) 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" ) 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() -
frontend_scaffolder.py 27.4 KB
#!/usr/bin/env python3 """ Frontend Project Scaffolder Generates a complete Next.js/React project structure with TypeScript, Tailwind CSS, and best practice configurations. Usage: python frontend_scaffolder.py my-app --template nextjs python frontend_scaffolder.py dashboard --template react --features auth,api python frontend_scaffolder.py landing --template nextjs --dry-run """ import argparse import json import os import sys from pathlib import Path from typing import Dict, List, Optional # Project templates TEMPLATES = { "nextjs": { "name": "Next.js 15+ App Router", "description": "Modern Next.js with App Router, Server Components, and TypeScript", "structure": { "app": { "layout.tsx": "ROOT_LAYOUT", "page.tsx": "HOME_PAGE", "globals.css": "GLOBALS_CSS", "(auth)": { "login": {"page.tsx": "AUTH_PAGE"}, "register": {"page.tsx": "AUTH_PAGE"}, }, "api": { "health": {"route.ts": "HEALTH_ROUTE"}, }, }, "components": { "ui": { "button.tsx": "UI_BUTTON", "input.tsx": "UI_INPUT", "card.tsx": "UI_CARD", "index.ts": "UI_INDEX", }, "layout": { "header.tsx": "LAYOUT_HEADER", "footer.tsx": "LAYOUT_FOOTER", "sidebar.tsx": "LAYOUT_SIDEBAR", }, }, "lib": { "utils.ts": "UTILS", "constants.ts": "CONSTANTS", }, "hooks": { "use-debounce.ts": "HOOK_DEBOUNCE", "use-local-storage.ts": "HOOK_LOCAL_STORAGE", }, "types": { "index.ts": "TYPES_INDEX", }, "public": { ".gitkeep": "EMPTY", }, }, "config_files": [ "next.config.js", "tailwind.config.ts", "tsconfig.json", "postcss.config.js", ".eslintrc.json", ".prettierrc", ".gitignore", "package.json", ], }, "react": { "name": "React + Vite", "description": "Modern React with Vite, TypeScript, and Tailwind CSS", "structure": { "src": { "App.tsx": "REACT_APP", "main.tsx": "REACT_MAIN", "index.css": "GLOBALS_CSS", "components": { "ui": { "button.tsx": "UI_BUTTON", "input.tsx": "UI_INPUT", "card.tsx": "UI_CARD", "index.ts": "UI_INDEX", }, }, "hooks": { "use-debounce.ts": "HOOK_DEBOUNCE", "use-local-storage.ts": "HOOK_LOCAL_STORAGE", }, "lib": { "utils.ts": "UTILS", }, "types": { "index.ts": "TYPES_INDEX", }, }, "public": { ".gitkeep": "EMPTY", }, }, "config_files": [ "vite.config.ts", "tailwind.config.ts", "tsconfig.json", "postcss.config.js", ".eslintrc.json", ".prettierrc", ".gitignore", "package.json", "index.html", ], }, } # Feature modules that can be added FEATURES = { "auth": { "description": "Authentication with session management", "files": { "lib/auth.ts": "AUTH_LIB", "middleware.ts": "AUTH_MIDDLEWARE", "components/auth/login-form.tsx": "LOGIN_FORM", "components/auth/register-form.tsx": "REGISTER_FORM", }, "dependencies": ["next-auth", "@auth/core"], }, "api": { "description": "API client with React Query", "files": { "lib/api-client.ts": "API_CLIENT", "lib/query-client.ts": "QUERY_CLIENT", "providers/query-provider.tsx": "QUERY_PROVIDER", }, "dependencies": ["@tanstack/react-query", "axios"], }, "forms": { "description": "Form handling with React Hook Form + Zod", "files": { "lib/form-utils.ts": "FORM_UTILS", "components/forms/form-field.tsx": "FORM_FIELD", }, "dependencies": ["react-hook-form", "@hookform/resolvers", "zod"], }, "testing": { "description": "Testing setup with Vitest and Testing Library", "files": { "vitest.config.ts": "VITEST_CONFIG", "src/test/setup.ts": "TEST_SETUP", "src/test/utils.tsx": "TEST_UTILS", }, "dependencies": ["vitest", "@testing-library/react", "@testing-library/jest-dom"], }, "storybook": { "description": "Component documentation with Storybook", "files": { ".storybook/main.ts": "STORYBOOK_MAIN", ".storybook/preview.ts": "STORYBOOK_PREVIEW", }, "dependencies": ["@storybook/react-vite", "@storybook/addon-essentials"], }, } # File content templates FILE_CONTENTS = { "ROOT_LAYOUT": '''import type { Metadata } from 'next'; import { Inter } from 'next/font/google'; import './globals.css'; const inter = Inter({ subsets: ['latin'], variable: '--font-inter' }); export const metadata: Metadata = { title: 'My App', description: 'Built with Next.js', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en"> <body className={`${inter.variable} font-sans antialiased`}> {children} </body> </html> ); } ''', "HOME_PAGE": '''export default function Home() { return ( <main className="flex min-h-screen flex-col items-center justify-center p-24"> <h1 className="text-4xl font-bold">Welcome</h1> <p className="mt-4 text-lg text-gray-600"> Get started by editing app/page.tsx </p> </main> ); } ''', "GLOBALS_CSS": '''@tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; --radius: 0.5rem; } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; } } ''', "UI_BUTTON": '''import { forwardRef } from 'react'; import { cn } from '@/lib/utils'; interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { variant?: 'default' | 'destructive' | 'outline' | 'ghost'; size?: 'default' | 'sm' | 'lg'; } const Button = forwardRef<HTMLButtonElement, ButtonProps>( ({ className, variant = 'default', size = 'default', ...props }, ref) => { return ( <button className={cn( 'inline-flex items-center justify-center rounded-md font-medium transition-colors', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', 'disabled:pointer-events-none disabled:opacity-50', { 'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'default', 'bg-destructive text-destructive-foreground hover:bg-destructive/90': variant === 'destructive', 'border border-input bg-background hover:bg-accent': variant === 'outline', 'hover:bg-accent hover:text-accent-foreground': variant === 'ghost', }, { 'h-10 px-4 py-2': size === 'default', 'h-9 px-3': size === 'sm', 'h-11 px-8': size === 'lg', }, className )} ref={ref} {...props} /> ); } ); Button.displayName = 'Button'; export { Button, type ButtonProps }; ''', "UI_INPUT": '''import { forwardRef } from 'react'; import { cn } from '@/lib/utils'; interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { error?: string; } const Input = forwardRef<HTMLInputElement, InputProps>( ({ className, error, ...props }, ref) => { return ( <div className="w-full"> <input className={cn( 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2', 'text-sm ring-offset-background file:border-0 file:bg-transparent', 'file:text-sm file:font-medium placeholder:text-muted-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', 'disabled:cursor-not-allowed disabled:opacity-50', error && 'border-destructive focus-visible:ring-destructive', className )} ref={ref} {...props} /> {error && <p className="mt-1 text-sm text-destructive">{error}</p>} </div> ); } ); Input.displayName = 'Input'; export { Input, type InputProps }; ''', "UI_CARD": '''import { cn } from '@/lib/utils'; interface CardProps extends React.HTMLAttributes<HTMLDivElement> {} function Card({ className, ...props }: CardProps) { return ( <div className={cn( 'rounded-lg border bg-card text-card-foreground shadow-sm', className )} {...props} /> ); } function CardHeader({ className, ...props }: CardProps) { return <div className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />; } function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) { return <h3 className={cn('text-2xl font-semibold leading-none', className)} {...props} />; } function CardContent({ className, ...props }: CardProps) { return <div className={cn('p-6 pt-0', className)} {...props} />; } function CardFooter({ className, ...props }: CardProps) { return <div className={cn('flex items-center p-6 pt-0', className)} {...props} />; } export { Card, CardHeader, CardTitle, CardContent, CardFooter }; ''', "UI_INDEX": '''export { Button } from './button'; export { Input } from './input'; export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './card'; ''', "UTILS": '''import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } export function formatDate(date: Date | string): string { return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric', }).format(new Date(date)); } export function sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } ''', "CONSTANTS": '''export const APP_NAME = 'My App'; export const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api'; export const ROUTES = { home: '/', login: '/login', register: '/register', dashboard: '/dashboard', } as const; export const QUERY_KEYS = { user: ['user'], products: ['products'], } as const; ''', "HOOK_DEBOUNCE": '''import { useState, useEffect } from 'react'; export function useDebounce<T>(value: T, delay: number = 500): T { const [debouncedValue, setDebouncedValue] = useState<T>(value); useEffect(() => { const timer = setTimeout(() => setDebouncedValue(value), delay); return () => clearTimeout(timer); }, [value, delay]); return debouncedValue; } ''', "HOOK_LOCAL_STORAGE": '''import { useState, useEffect } from 'react'; export function useLocalStorage<T>( key: string, initialValue: T ): [T, (value: T | ((prev: T) => T)) => void] { 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; } }); useEffect(() => { if (typeof window !== 'undefined') { window.localStorage.setItem(key, JSON.stringify(storedValue)); } }, [key, storedValue]); return [storedValue, setStoredValue]; } ''', "TYPES_INDEX": '''export interface User { id: string; email: string; name: string; createdAt: Date; } export interface ApiResponse<T> { data: T; message?: string; error?: string; } export interface PaginatedResponse<T> { data: T[]; total: number; page: number; pageSize: number; totalPages: number; } ''', "HEALTH_ROUTE": '''import { NextResponse } from 'next/server'; export async function GET() { return NextResponse.json({ status: 'ok', timestamp: new Date().toISOString(), }); } ''', "AUTH_PAGE": ''''use client'; export default function AuthPage() { return ( <div className="flex min-h-screen items-center justify-center"> <div className="w-full max-w-md p-8"> <h1 className="text-2xl font-bold text-center">Authentication</h1> </div> </div> ); } ''', "LAYOUT_HEADER": '''import Link from 'next/link'; export function Header() { return ( <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur"> <div className="container flex h-14 items-center"> <Link href="/" className="font-bold"> Logo </Link> <nav className="ml-auto flex gap-4"> <Link href="/about" className="text-sm text-muted-foreground hover:text-foreground"> About </Link> </nav> </div> </header> ); } ''', "LAYOUT_FOOTER": '''export function Footer() { return ( <footer className="border-t py-6"> <div className="container text-center text-sm text-muted-foreground"> <p>© {new Date().getFullYear()} My App. All rights reserved.</p> </div> </footer> ); } ''', "LAYOUT_SIDEBAR": '''interface SidebarProps { children?: React.ReactNode; } export function Sidebar({ children }: SidebarProps) { return ( <aside className="fixed left-0 top-14 z-30 h-[calc(100vh-3.5rem)] w-64 border-r bg-background"> <div className="p-4">{children}</div> </aside> ); } ''', "REACT_APP": '''import { Button } from './components/ui'; function App() { return ( <main className="flex min-h-screen flex-col items-center justify-center p-24"> <h1 className="text-4xl font-bold">Welcome</h1> <p className="mt-4 text-lg text-gray-600"> Get started by editing src/App.tsx </p> <Button className="mt-6">Get Started</Button> </main> ); } export default App; ''', "REACT_MAIN": '''import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <App /> </React.StrictMode> ); ''', "EMPTY": "", } def generate_structure( base_path: Path, structure: Dict, dry_run: bool = False ) -> List[str]: """Generate directory structure recursively.""" created_files = [] for name, content in structure.items(): current_path = base_path / name if isinstance(content, dict): # It's a directory if not dry_run: current_path.mkdir(parents=True, exist_ok=True) created_files.extend(generate_structure(current_path, content, dry_run)) else: # It's a file if not dry_run: current_path.parent.mkdir(parents=True, exist_ok=True) file_content = FILE_CONTENTS.get(content, "") current_path.write_text(file_content) created_files.append(str(current_path)) return created_files def generate_config_files( project_path: Path, template: str, project_name: str, features: List[str], dry_run: bool = False ) -> List[str]: """Generate configuration files.""" created_files = [] config_templates = get_config_templates(project_name, template, features) template_config = TEMPLATES[template] for config_file in template_config["config_files"]: file_path = project_path / config_file if config_file in config_templates: if not dry_run: file_path.write_text(config_templates[config_file]) created_files.append(str(file_path)) return created_files def get_config_templates(name: str, template: str, features: List[str]) -> Dict[str, str]: """Get configuration file contents.""" deps = { "nextjs": { "dependencies": { "next": "^14.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "clsx": "^2.0.0", "tailwind-merge": "^2.0.0", }, "devDependencies": { "@types/node": "^20.0.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "autoprefixer": "^10.0.0", "eslint": "^8.0.0", "eslint-config-next": "^14.0.0", "postcss": "^8.0.0", "prettier": "^3.0.0", "tailwindcss": "^3.4.0", "typescript": "^5.0.0", }, }, "react": { "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", "clsx": "^2.0.0", "tailwind-merge": "^2.0.0", }, "devDependencies": { "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.0.0", "autoprefixer": "^10.0.0", "eslint": "^8.0.0", "postcss": "^8.0.0", "prettier": "^3.0.0", "tailwindcss": "^3.4.0", "typescript": "^5.0.0", "vite": "^5.0.0", }, }, } # Add feature dependencies for feature in features: if feature in FEATURES: for dep in FEATURES[feature].get("dependencies", []): deps[template]["dependencies"][dep] = "latest" package_json = { "name": name, "version": "0.1.0", "private": True, "scripts": { "dev": "next dev" if template == "nextjs" else "vite", "build": "next build" if template == "nextjs" else "vite build", "start": "next start" if template == "nextjs" else "vite preview", "lint": "eslint . --ext .ts,.tsx", "format": "prettier --write .", }, "dependencies": deps[template]["dependencies"], "devDependencies": deps[template]["devDependencies"], } return { "package.json": json.dumps(package_json, indent=2), "tsconfig.json": '''{ "compilerOptions": { "target": "ES2020", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, "plugins": [{ "name": "next" }], "paths": { "@/*": ["./*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } ''', "tailwind.config.ts": '''import type { Config } from 'tailwindcss'; const config: Config = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', './src/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { extend: { colors: { background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))', primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))', }, secondary: { DEFAULT: 'hsl(var(--secondary))', foreground: 'hsl(var(--secondary-foreground))', }, destructive: { DEFAULT: 'hsl(var(--destructive))', foreground: 'hsl(var(--destructive-foreground))', }, muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))', }, accent: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-foreground))', }, border: 'hsl(var(--border))', ring: 'hsl(var(--ring))', }, borderRadius: { lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)', }, }, }, plugins: [], }; export default config; ''', "postcss.config.js": '''module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ''', "next.config.js": '''/** @type {import('next').NextConfig} */ const nextConfig = { images: { remotePatterns: [], formats: ['image/avif', 'image/webp'], }, experimental: { optimizePackageImports: ['lucide-react'], }, }; module.exports = nextConfig; ''', "vite.config.ts": '''import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'path'; export default defineConfig({ plugins: [react()], resolve: { alias: { '@': path.resolve(__dirname, './src'), }, }, }); ''', ".eslintrc.json": '''{ "extends": ["next/core-web-vitals", "prettier"], "rules": { "react/no-unescaped-entities": "off" } } ''', ".prettierrc": '''{ "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "es5", "printWidth": 100 } ''', ".gitignore": '''# Dependencies node_modules/ .pnp .pnp.js # Build .next/ out/ dist/ build/ # Environment .env .env.local .env.*.local # IDE .vscode/ .idea/ # Debug npm-debug.log* yarn-debug.log* yarn-error.log* # OS .DS_Store Thumbs.db # Testing coverage/ ''', "index.html": '''<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>''' + name + '''</title> </head> <body> <div id="root"></div> <script type="module" src="/src/main.tsx"></script> </body> </html> ''', } 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"export const {content_key.replace('-', '_')} = () => {{\n /* Functional implementation for {content_key} */\n}};\n") 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(): parser = argparse.ArgumentParser( description="Scaffold a frontend project with best practices" ) parser.add_argument( "name", help="Project name (kebab-case recommended)" ) parser.add_argument( "--dir", "-d", default=".", help="Output directory (default: current directory)" ) parser.add_argument( "--template", "-t", choices=list(TEMPLATES.keys()), default="nextjs", help="Project template (default: nextjs)" ) parser.add_argument( "--features", "-f", help="Comma-separated features to add (auth,api,forms,testing,storybook)" ) 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" ) 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()
-
-
SKILL.md 7.9 KB
--- name: senior-frontend description: "Frontend development for React 19, Next.js 15, TypeScript, and Tailwind CSS v4 / Pengembangan frontend dengan React 19, Next.js 15, TypeScript, dan Tailwind CSS v4." author: "Roedy Rustam" version: "4.0.0" --- # Senior Frontend Specialist (2026 Edition) [English](#english) | [Bahasa Indonesia](#bahasa-indonesia) --- <a name="english"></a> ## English ### Description Production-grade frontend development patterns, performance optimization, and modern ecosystem integrations for React 19 / Next.js 15 applications with Tailwind CSS v4 and TypeScript. Covers React 19 Compiler, Partial Prerendering (PPR), View Transitions API, and SPA/MPA hybrid strategies with automated self-healing testing loops. ### Orchestration & Integration - `design-system-architect`: For building robust UI components (Radix, Base UI, shadcn) and design tokens. - `tailwind-expert`: For styling and custom @theme configurations. - `svg-animation-motion-expert`: For advanced animations, GSAP timelines, and motion physics. - `nextjs-app-router-expert`: For RSC, caching, and Next.js routing patterns. - `state-management-expert`: For client-side state (Zustand, Jotai). - `tanstack-query-expert`: For advanced data fetching and server state caching. - `spa-orchestrator`: For Single-Page Application architectures. - `mpa-orchestrator`: For Multi-Page Application architectures. - `autonomous-tdd-debugger`: For automated unit/component test execution and stack-trace self-healing. ### Trigger Conditions - Scaffold a new React or Next.js 15 project with TypeScript and Tailwind CSS v4. - Generate new components, custom hooks, or Server Actions. - Analyze and optimize bundle sizes and Core Web Vitals. - Implement advanced React 19 patterns (`useActionState`, `useOptimistic`, `use()`). - Build SPAs — coordinate with `spa-orchestrator`. - Ensure accessibility compliance (WCAG 2.2) and testing. ### Technical Guidelines & Best Practices #### 1. Next.js 15: Server vs Client Components Use **Server Components** by default for performance and SEO. Use `'use client'` strictly for: - State (`useState`, `useEffect`), event handlers (`onClick`), browser APIs. - Interactive UI needing client hydration. *Next.js 15.2+ Requirements:* - Await `Promise`-based `params` and `searchParams` before accessing properties. - Use **Partial Prerendering (PPR)** natively. Wrap dynamic sections in `<Suspense>`. #### 2. React 19 Compiler & Hooks Do not use `useMemo`, `useCallback`, or `React.memo` unless explicitly required; rely on the React 19 compiler. **Mandatory Hooks:** - **`useActionState`**: Manage form state, pending indicators, and action results with Server Actions. - **`useOptimistic`**: Update UI instantly before server confirmation. - **`useFormStatus`**: Read form submission state in child components. - **`use(promise)`**: Unwrap promises or context inside render with Suspense. #### 3. View Transitions API Implement native View Transitions API for smooth page transitions. Use Next.js 15 `<Link viewTransition>` natively. #### 4. Tailwind CSS v4 Use CSS-first configuration. Define custom tokens with `@theme` and register plugins with `@plugin` in the main CSS file. Never create `tailwind.config.js`. #### 5. Accessibility (WCAG 2.2) & Self-Healing Testing - Enforce semantic HTML (`<button>`, `<nav>`, `<main>`, `<article>`). - Enforce full keyboard navigability and valid `aria-*` labels. - Run typecheck (`tsc --noEmit`) to verify zero compiler diagnostics. - Write unit tests using **Vitest** and **React Testing Library**. - When tests fail, activate `autonomous-tdd-debugger` to self-heal until 100% passing. #### 6. Architecture Integration - **SPA (`spa-orchestrator`)**: Use **TanStack Router** for type-safe client routing and **TanStack Query v5** for server state. Never use bare `useEffect` for data fetching. - **MPA (`mpa-orchestrator`)**: Use **Alpine.js** or **HTMX** for micro-interactions. Apply Progressive Enhancement. #### 7. Animations & Motion For web animations, scroll-driven timelines, GSAP choreographies, and Framer Motion spring physics, delegate directly to `svg-animation-motion-expert`. --- <a name="bahasa-indonesia"></a> ## Bahasa Indonesia ### Deskripsi Pola pengembangan frontend tingkat produksi, optimasi performa, dan integrasi ekosistem modern untuk aplikasi React 19 / Next.js 15 dengan Tailwind CSS v4 dan TypeScript. Mencakup React 19 Compiler, Partial Prerendering (PPR), View Transitions API, dan strategi hybrid SPA/MPA dengan loop pengujian perbaikan mandiri (self-healing). ### Integrasi Orkestrasi - `design-system-architect`: Untuk membangun komponen UI yang kuat (Radix, Base UI, shadcn) dan design token. - `tailwind-expert`: Untuk styling dan konfigurasi @theme kustom. - `svg-animation-motion-expert`: Untuk animasi web, timeline GSAP, dan fisika spring. - `nextjs-app-router-expert`: Untuk RSC, caching, dan pola routing Next.js. - `state-management-expert`: Untuk state client-side (Zustand, Jotai). - `tanstack-query-expert`: Untuk fetching data lanjutan dan caching server state. - `spa-orchestrator`: Untuk arsitektur Single-Page Application. - `mpa-orchestrator`: Untuk arsitektur Multi-Page Application. - `autonomous-tdd-debugger`: Untuk eksekusi pengujian otomatis dan perbaikan mandiri berdasarkan stack trace terminal. ### Kondisi Pemicu - Buat proyek React atau Next.js 15 baru dengan TypeScript dan Tailwind CSS v4. - Buat komponen baru, custom hooks, atau Server Actions. - Analisis dan optimalkan ukuran bundle dan Core Web Vitals. - Terapkan pola React 19 lanjutan (`useActionState`, `useOptimistic`, `use()`). - Bangun SPA — koordinasikan dengan `spa-orchestrator`. - Pastikan kepatuhan aksesibilitas (WCAG 2.2) dan pengujian. ### Panduan Teknis & Praktik Terbaik #### 1. Next.js 15: Server vs Client Components Gunakan **Server Components** secara default. Gunakan `'use client'` hanya untuk: - State (`useState`, `useEffect`), event handler, API browser. - UI interaktif yang membutuhkan hidrasi klien. *Persyaratan Next.js 15.2+:* - Wajib `await` pada `params` dan `searchParams` yang kini bertipe `Promise`. - Gunakan **Partial Prerendering (PPR)** secara native. Bungkus bagian dinamis dengan `<Suspense>`. #### 2. React 19 Compiler & Hook Baru Jangan gunakan `useMemo`, `useCallback`, atau `React.memo` kecuali sangat diperlukan; andalkan compiler React 19. **Hook Wajib:** - **`useActionState`**: Kelola state form, indikator loading, dan hasil action dengan Server Actions. - **`useOptimistic`**: Perbarui UI secara instan sebelum server mengonfirmasi. - **`useFormStatus`**: Baca status form di dalam komponen anak. - **`use(promise)`**: Buka promise atau context langsung di dalam render dengan Suspense. #### 3. View Transitions API Terapkan View Transitions API bawaan browser untuk transisi halaman. Gunakan `<Link viewTransition>` pada Next.js 15. #### 4. Tailwind CSS v4 Gunakan konfigurasi CSS-first. Definisikan token kustom dengan `@theme` dan plugin dengan `@plugin` di file CSS utama (lihat `tailwind-expert`). Jangan pernah membuat file `tailwind.config.js`. #### 5. Aksesibilitas (WCAG 2.2) & Pengujian Self-Healing - Wajib gunakan HTML semantik (`<button>`, `<nav>`, `<main>`). - Wajib pastikan navigasi keyboard dengan label `aria-*` yang valid. - Jalankan pemeriksaan tipe (`tsc --noEmit`) untuk memastikan nol eror diagnostik. - Tulis pengujian unit dengan **Vitest** dan **React Testing Library**. - Jika pengujian gagal, aktifkan `autonomous-tdd-debugger` untuk memulihkan kode hingga 100% lolos. #### 6. Integrasi Arsitektur - **SPA (`spa-orchestrator`)**: Gunakan **TanStack Router** untuk routing klien dan **TanStack Query v5** untuk server state. Jangan gunakan `useEffect` murni untuk fetching data. - **MPA (`mpa-orchestrator`)**: Gunakan **Alpine.js** atau **HTMX** untuk interaksi mikro. Terapkan Progressive Enhancement. #### 7. Animasi & Motion Untuk animasi web, timeline scroll-driven, koreografi GSAP, dan fisika spring Framer Motion, delegasikan langsung ke `svg-animation-motion-expert`.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.