
Senior Frontend
- 284 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
senior-frontend is a Claude skill that ships accessible, performant UIs with modern component architecture, state management, and design-system discipline for developers building web products and dashboards.
About
senior-frontend is a Claude skill from borghei/claude-skills that applies staff-level frontend engineering practices to web products and dashboards. The skill covers accessible markup, performance budgets, component architecture, state management patterns, and design-system consistency across React or modern SPA stacks. Developers reach for senior-frontend when refactoring UI foundations, hardening production interfaces, or elevating implementation quality beyond basic component scaffolding.
- Applies component-driven architecture and design tokens
- Prioritizes accessibility, performance, and responsive layout
- Chooses pragmatic state and data-fetch patterns
- Reviews UX edge cases and loading or error states
- Integrates cleanly with API contracts and auth flows
Senior Frontend by the numbers
- 284 all-time installs (skills.sh)
- Ranked #762 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/borghei/claude-skills --skill senior-frontendAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 284 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
How do you build production-grade accessible frontend UIs?
Ship accessible, performant UIs with modern component architecture, state management, and design-system discipline for web products and dashboards.
Who is it for?
Experienced developers shipping production web apps who need staff-level frontend architecture and accessibility discipline.
Skip if: Beginners learning HTML basics or backend-only engineers who will not touch UI code.
When should I use this skill?
A developer asks for accessible UI patterns, frontend architecture, design-system structure, or performance-focused component design.
What you get
Component architecture plan, accessible UI implementation, and design-system-aligned frontend patterns
- ui component architecture
- accessible interface implementation
Files
Senior Frontend
Frontend development patterns, performance optimization, and automation tools for React/Next.js applications. Scaffold projects, generate components and hooks, analyze bundle sizes, and apply React/Next.js patterns with accessibility and testing built in.
Core Capabilities
- Project scaffolding — generate Next.js 14+ (App Router) or React+Vite projects with TypeScript, Tailwind, and optional auth/api/forms/testing/storybook features.
- Component generation — client/server components, custom hooks, with test and Storybook story files following established patterns.
- Bundle analysis — static
package.json+ import scanning that scores bundle health and flags heavy dependencies with lighter alternatives. - React patterns — compound components, custom hooks, render props, and reusable state-sharing patterns.
- Next.js optimization — Server vs Client Components, image optimization, parallel/streaming data fetching with Suspense.
- Accessibility & testing — semantic HTML, ARIA, keyboard nav, focus, and React Testing Library component/a11y tests.
When to Use
- Starting a new React/Next.js project that needs a best-practice baseline.
- Adding components, hooks, or test/story scaffolding to an existing app.
- Optimizing bundle size or diagnosing heavy dependencies.
- Implementing accessible, performant Server/Client Component architecture.
- Reviewing frontend code against React/TypeScript/a11y best practices.
Clarify First
Before scaffolding or generating, confirm these inputs. If any is unknown or vague, ASK — do not assume:
- [ ] Template — Next.js (App Router) vs React+Vite (
--template; produces a different project structure) - [ ] Features — auth / api / forms / testing / storybook (
--features; decides what gets generated) - [ ] Generation target — a new project vs a component/hook in an existing app (selects
frontend_scaffoldervscomponent_generator)
Stop rule: ask only the 2-3 that most change the output. If the user says "just draft it," proceed and list your assumptions at the top of the artifact.
Tools
| Tool | Purpose | Command |
|---|---|---|
frontend_scaffolder.py | Scaffold a Next.js or React project with TypeScript + Tailwind + optional features | python scripts/frontend_scaffolder.py my-app --template nextjs --features auth,api |
component_generator.py | Generate a component/hook with optional test and Storybook story | python scripts/component_generator.py ProductCard --type client --with-test --with-story |
bundle_analyzer.py | Score bundle health and flag heavy dependencies from package.json + imports | python scripts/bundle_analyzer.py /path/to/project --verbose |
References
Load the reference that matches the task — keep this file lean and pull detail on demand:
- [references/tooling-guide.md](references/tooling-guide.md) — full scaffolding / component-generation / bundle-analysis workflows, option tables, generated structure, the per-script flag reference, troubleshooting table, and success criteria. Read when running a tool or wiring up CLI flags.
- [references/code-patterns.md](references/code-patterns.md) — inline React patterns (compound components, hooks, render props), Next.js optimization (Server/Client, image, data fetching), accessibility + testing snippets, and the Next.js config / Tailwind / TypeScript quick reference. Read when writing components or optimizing pages.
- [references/react_patterns.md](references/react_patterns.md) — deep React patterns library. Read when designing component/state architecture.
- [references/nextjs_optimization_guide.md](references/nextjs_optimization_guide.md) — deep Next.js performance and rendering guide. Read when tuning Next.js apps.
- [references/frontend_best_practices.md](references/frontend_best_practices.md) — accessibility, testing, and general frontend best-practice guide. Read before shipping a component.
Scope & Limitations
What this skill covers:
- React and Next.js project scaffolding with TypeScript and Tailwind CSS
- Component, hook, test, and Storybook story generation following established patterns
- Static bundle analysis based on
package.jsondependency inspection and import pattern scanning - Frontend-specific best practices for Server Components, image optimization, data fetching, and accessibility
What this skill does NOT cover:
- Backend API development, database schema design, or server infrastructure -- see senior-backend and senior-fullstack
- End-to-end testing with Cypress or Playwright -- see senior-qa
- CI/CD pipeline configuration and Docker deployment -- see senior-devops
- Security vulnerability scanning and penetration testing -- see senior-secops and senior-security
Integration Points
| Skill | Integration | Data Flow |
|---|---|---|
| senior-fullstack | Scaffolded frontend projects connect to fullstack project scaffolder for API layer setup | Frontend project structure feeds into project_scaffolder.py which adds backend, Docker, and CI/CD layers |
| senior-backend | Components consuming API data follow patterns defined by backend skill's REST/GraphQL conventions | Backend API response types imported into frontend types/ directory generated by this skill |
| senior-qa | Generated test files (--with-test) use the same Testing Library conventions that the QA skill's test strategies build upon | Component test files hand off to QA skill for integration and E2E test coverage expansion |
| senior-devops | Bundle analyzer output informs build pipeline optimization decisions | Bundle health score and dependency warnings feed into CI quality gates configured by DevOps skill |
| senior-secops | Dependency analysis identifies packages that need security audit | Heavy/outdated dependency warnings from bundle_analyzer.py trigger security review workflows |
| code-reviewer | Generated components follow patterns that the code reviewer skill validates | Code reviewer checks generated components against React/TypeScript best practices defined in this skill's references |
Frontend Code Patterns
Read this when writing React components, Next.js Server/Client Components, image and data-fetching code, accessibility markup, component tests, or you need the quick-reference Next.js config / Tailwind / TypeScript snippets. For deeper coverage see the companion references: react_patterns.md, nextjs_optimization_guide.md, and frontend_best_practices.md.
React Patterns
Reference: references/react_patterns.md
Compound Components
Share state between related components:
const Tabs = ({ children }) => {
const [active, setActive] = useState(0);
return (
<TabsContext.Provider value={{ active, setActive }}>
{children}
</TabsContext.Provider>
);
};
Tabs.List = TabList;
Tabs.Panel = TabPanel;
// Usage
<Tabs>
<Tabs.List>
<Tabs.Tab>One</Tabs.Tab>
<Tabs.Tab>Two</Tabs.Tab>
</Tabs.List>
<Tabs.Panel>Content 1</Tabs.Panel>
<Tabs.Panel>Content 2</Tabs.Panel>
</Tabs>Custom Hooks
Extract reusable logic:
function useDebounce<T>(value: T, delay = 500): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Usage
const debouncedSearch = useDebounce(searchTerm, 300);Render Props
Share rendering logic:
function DataFetcher({ url, render }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url).then(r => r.json()).then(setData).finally(() => setLoading(false));
}, [url]);
return render({ data, loading });
}
// Usage
<DataFetcher
url="/api/users"
render={({ data, loading }) =>
loading ? <Spinner /> : <UserList users={data} />
}
/>---
Next.js Optimization
Reference: references/nextjs_optimization_guide.md
Server vs Client Components
Use Server Components by default. Add 'use client' only when you need:
- Event handlers (onClick, onChange)
- State (useState, useReducer)
- Effects (useEffect)
- Browser APIs
// Server Component (default) - no 'use client'
async function ProductPage({ params }) {
const product = await getProduct(params.id); // Server-side fetch
return (
<div>
<h1>{product.name}</h1>
<AddToCartButton productId={product.id} /> {/* Client component */}
</div>
);
}
// Client Component
'use client';
function AddToCartButton({ productId }) {
const [adding, setAdding] = useState(false);
return <button onClick={() => addToCart(productId)}>Add</button>;
}Image Optimization
import Image from 'next/image';
// Above the fold - load immediately
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority
/>
// Responsive image with fill
<div className="relative aspect-video">
<Image
src="/product.jpg"
alt="Product"
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover"
/>
</div>Data Fetching Patterns
// Parallel fetching
async function Dashboard() {
const [user, stats] = await Promise.all([
getUser(),
getStats()
]);
return <div>...</div>;
}
// Streaming with Suspense
async function ProductPage({ params }) {
return (
<div>
<ProductDetails id={params.id} />
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={params.id} />
</Suspense>
</div>
);
}---
Accessibility and Testing
Reference: references/frontend_best_practices.md
Accessibility Checklist
1. Semantic HTML: Use proper elements (<button>, <nav>, <main>) 2. Keyboard Navigation: All interactive elements focusable 3. ARIA Labels: Provide labels for icons and complex widgets 4. Color Contrast: Minimum 4.5:1 for normal text 5. Focus Indicators: Visible focus states
// Accessible button
<button
type="button"
aria-label="Close dialog"
onClick={onClose}
className="focus-visible:ring-2 focus-visible:ring-blue-500"
>
<XIcon aria-hidden="true" />
</button>
// Skip link for keyboard users
<a href="#main-content" className="sr-only focus:not-sr-only">
Skip to main content
</a>Testing Strategy
// Component test with React Testing Library
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('button triggers action on click', async () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Click me</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});
// Test accessibility
test('dialog is accessible', async () => {
render(<Dialog open={true} title="Confirm" />);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByRole('dialog')).toHaveAttribute('aria-labelledby');
});---
Quick Reference
Common Next.js Config
// next.config.js
const nextConfig = {
images: {
remotePatterns: [{ hostname: 'cdn.example.com' }],
formats: ['image/avif', 'image/webp'],
},
experimental: {
optimizePackageImports: ['lucide-react', '@heroicons/react'],
},
};Tailwind CSS Utilities
// Conditional classes with cn()
import { cn } from '@/lib/utils';
<button className={cn(
'px-4 py-2 rounded',
variant === 'primary' && 'bg-blue-500 text-white',
disabled && 'opacity-50 cursor-not-allowed'
)} />TypeScript Patterns
// Props with children
interface CardProps {
className?: string;
children: React.ReactNode;
}
// Generic component
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>;
}Frontend Best Practices
Modern frontend development standards for accessibility, testing, TypeScript, and Tailwind CSS.
---
Table of Contents
---
Accessibility (a11y)
Semantic HTML
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
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
// 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
// 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.cssBarrel Exports
// 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
// 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
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
// 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());
}Next.js Optimization Guide
Performance optimization techniques for Next.js 14+ applications.
---
Table of Contents
- Rendering Strategies
- Image Optimization
- Code Splitting
- Data Fetching
- Caching Strategies
- Bundle Optimization
- 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.
// 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)
'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
// 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
// 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
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
// 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
// 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
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
// 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// 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
// 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
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
// 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
// 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
// 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
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
# 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 buildTree Shaking Imports
// 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
// 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
// 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)
// 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)
// 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)
// 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
// 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
// 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
Production-ready patterns for building scalable React applications with TypeScript.
---
Table of Contents
---
Component Composition
Compound Components
Use compound components when building reusable UI components with multiple related parts.
// 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.
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.
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
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
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
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
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
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.
// 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)
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
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
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
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
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
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
function DataComponent() {
return (
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<LoadingSpinner />}>
<AsyncDataLoader />
</Suspense>
</ErrorBoundary>
);
}---
Anti-Patterns
Avoid: Inline Object/Array Creation in JSX
// 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
// 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
// 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
// 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
// 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]
);Frontend Tooling Guide
Read this when scaffolding a project, generating components, analyzing bundles, or you need the full CLI flag reference, troubleshooting table, or success criteria for the three scripts.
Project Scaffolding
Generate a new Next.js or React project with TypeScript, Tailwind CSS, and best practice configurations.
Workflow: Create New Frontend Project
1. Run the scaffolder with your project name and template:
python scripts/frontend_scaffolder.py my-app --template nextjs2. Add optional features (auth, api, forms, testing, storybook):
python scripts/frontend_scaffolder.py dashboard --template nextjs --features auth,api3. Navigate to the project and install dependencies:
cd my-app && npm install4. Start the development server:
npm run devScaffolder Options
| Option | Description |
|---|---|
--template nextjs | Next.js 14+ with App Router and Server Components |
--template react | React + Vite with TypeScript |
--features auth | Add NextAuth.js authentication |
--features api | Add React Query + API client |
--features forms | Add React Hook Form + Zod validation |
--features testing | Add Vitest + Testing Library |
--dry-run | Preview files without creating them |
Generated Structure (Next.js)
my-app/
├── app/
│ ├── layout.tsx # Root layout with fonts
│ ├── page.tsx # Home page
│ ├── globals.css # Tailwind + CSS variables
│ └── api/health/route.ts
├── components/
│ ├── ui/ # Button, Input, Card
│ └── layout/ # Header, Footer, Sidebar
├── hooks/ # useDebounce, useLocalStorage
├── lib/ # utils (cn), constants
├── types/ # TypeScript interfaces
├── tailwind.config.ts
├── next.config.js
└── package.json---
Component Generation
Generate React components with TypeScript, tests, and Storybook stories.
Workflow: Create a New Component
1. Generate a client component:
python scripts/component_generator.py Button --dir src/components/ui2. Generate a server component:
python scripts/component_generator.py ProductCard --type server3. Generate with test and story files:
python scripts/component_generator.py UserProfile --with-test --with-story4. Generate a custom hook:
python scripts/component_generator.py FormValidation --type hookGenerator Options
| Option | Description |
|---|---|
--type client | Client component with 'use client' (default) |
--type server | Async server component |
--type hook | Custom React hook |
--with-test | Include test file |
--with-story | Include Storybook story |
--flat | Create in output dir without subdirectory |
--dry-run | Preview without creating files |
Generated Component Example
'use client';
import { useState } from 'react';
import { cn } from '@/lib/utils';
interface ButtonProps {
className?: string;
children?: React.ReactNode;
}
export function Button({ className, children }: ButtonProps) {
return (
<div className={cn('', className)}>
{children}
</div>
);
}---
Bundle Analysis
Analyze package.json and project structure for bundle optimization opportunities.
Workflow: Optimize Bundle Size
1. Run the analyzer on your project:
python scripts/bundle_analyzer.py /path/to/project2. Review the health score and issues:
Bundle Health Score: 75/100 (C)
HEAVY DEPENDENCIES:
moment (290KB)
Alternative: date-fns (12KB) or dayjs (2KB)
lodash (71KB)
Alternative: lodash-es with tree-shaking3. Apply the recommended fixes by replacing heavy dependencies.
4. Re-run with verbose mode to check import patterns:
python scripts/bundle_analyzer.py . --verboseBundle Score Interpretation
| Score | Grade | Action |
|---|---|---|
| 90-100 | A | Bundle is well-optimized |
| 80-89 | B | Minor optimizations available |
| 70-79 | C | Replace heavy dependencies |
| 60-69 | D | Multiple issues need attention |
| 0-59 | F | Critical bundle size problems |
Heavy Dependencies Detected
The analyzer identifies these common heavy packages:
| Package | Size | Alternative |
|---|---|---|
| moment | 290KB | date-fns (12KB) or dayjs (2KB) |
| lodash | 71KB | lodash-es with tree-shaking |
| axios | 14KB | Native fetch or ky (3KB) |
| jquery | 87KB | Native DOM APIs |
| @mui/material | Large | shadcn/ui or Radix UI |
---
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Scaffolder fails with "Directory already exists" | Target project folder already present on disk | Delete or rename the existing directory, or choose a different project name |
| Component generator creates PascalCase name from kebab-case incorrectly | Input contains mixed delimiters (e.g., my_comp-name) | Use consistent kebab-case (my-comp-name) or PascalCase (MyCompName) as input |
| Bundle analyzer reports "No valid package.json found" | Script is pointed at a directory without package.json or the file has invalid JSON | Pass the correct project root directory; validate package.json syntax with python -m json.tool package.json |
--dry-run shows files but --features content is listed as TODO | Feature file content keys are not mapped in FILE_CONTENTS dictionary | This is expected for some add-on features; implement the placeholder files manually after scaffolding |
| Bundle score unexpectedly low despite few dependencies | Dev-only packages (TypeScript, ESLint, Tailwind) are listed under dependencies instead of devDependencies | Move build/dev tooling to devDependencies in package.json |
| Import analysis returns zero files checked | Source code is not in src/, app/, or pages/ directories | Run with --verbose and ensure your source files live in one of the three expected directories |
Generated component missing 'use client' directive | Component was generated with --type server instead of the default client type | Re-run with --type client or add the 'use client' directive manually at the top of the file |
---
Success Criteria
- Lighthouse performance score above 90 on the generated project's production build, indicating Server Components and image optimization are configured correctly.
- Bundle size under 200KB gzipped for the initial JavaScript payload, validated by running the bundle analyzer with a grade of A or B.
- Zero heavy-dependency warnings from
bundle_analyzer.pyafter applying all recommended replacements. - Component generation time under 2 seconds per component, including test and story file creation.
- All generated TypeScript files pass `tsc --noEmit` without errors, confirming type-safe scaffolding output.
- Accessibility audit produces zero critical violations when running axe-core or Lighthouse accessibility checks against generated components.
- Test coverage above 80% for generated components when the
--with-testflag is used and tests are executed with Vitest.
---
Tool Reference
frontend_scaffolder.py
- Purpose: Scaffold a complete Next.js or React project with TypeScript, Tailwind CSS, and optional feature modules.
- Usage:
python scripts/frontend_scaffolder.py <name> [flags] - Flags:
| Flag | Type | Default | Description |
|---|---|---|---|
name | positional | (required) | Project name, kebab-case recommended |
--dir, -d | string | . | Output directory where the project folder is created |
--template, -t | choice | nextjs | Project template: nextjs or react |
--features, -f | string | (none) | Comma-separated features: auth, api, forms, testing, storybook |
--list-templates | flag | off | List available project templates and exit |
--list-features | flag | off | List available feature modules and exit |
--dry-run | flag | off | Preview generated file list without writing to disk |
--json | flag | off | Output result as JSON instead of human-readable summary |
- Example:
python scripts/frontend_scaffolder.py dashboard --template nextjs --features auth,api --json {
"name": "dashboard",
"template": "nextjs",
"template_name": "Next.js 14+ App Router",
"features": ["auth", "api"],
"path": "./dashboard",
"files_created": 28,
"next_steps": ["cd dashboard", "npm install", "npm run dev"]
}- Output Formats: Human-readable summary (default) or JSON (
--json).
---
component_generator.py
- Purpose: Generate React/Next.js component files with TypeScript, optional test, and Storybook story.
- Usage:
python scripts/component_generator.py <name> [flags] - Flags:
| Flag | Type | Default | Description |
|---|---|---|---|
name | positional | (required) | Component name in PascalCase or kebab-case |
--dir, -d | string | src/components | Output directory for generated files |
--type, -t | choice | client | Component type: client, server, or hook |
--with-test | flag | off | Generate a .test.tsx file with Testing Library boilerplate |
--with-story | flag | off | Generate a .stories.tsx file for Storybook |
--no-index | flag | off | Skip generating the index.ts barrel export file |
--flat | flag | off | Place files directly in output dir without creating a subdirectory |
--dry-run | flag | off | Preview what would be generated without writing files |
--verbose, -v | flag | off | Enable verbose output |
- Example:
python scripts/component_generator.py ProductCard --dir src/components/ui --type client --with-test --with-story ==================================================
Component Generated: ProductCard
==================================================
Type: client
Directory: src/components/ui/ProductCard
Files created:
- src/components/ui/ProductCard/ProductCard.tsx
- src/components/ui/ProductCard/ProductCard.test.tsx
- src/components/ui/ProductCard/ProductCard.stories.tsx
- src/components/ui/ProductCard/index.ts
==================================================- Output Formats: Human-readable summary only.
---
bundle_analyzer.py
- Purpose: Analyze
package.jsonand project source files for bundle size issues, heavy dependencies, and optimization opportunities. - Usage:
python scripts/bundle_analyzer.py [project_dir] [flags] - Flags:
| Flag | Type | Default | Description |
|---|---|---|---|
project_dir | positional | . | Project directory containing package.json |
--json | flag | off | Output full analysis as JSON |
--verbose, -v | flag | off | Include detailed import pattern analysis across src/, app/, and pages/ directories |
- Example:
python scripts/bundle_analyzer.py /path/to/my-app --verbose ============================================================
FRONTEND BUNDLE ANALYSIS REPORT
============================================================
Bundle Health Score: 70/100 (C)
Dependencies: 12 production, 18 dev
--- HEAVY DEPENDENCIES ---
moment (290KB)
Reason: Large locale files bundled by default
Alternative: date-fns (12KB) or dayjs (2KB)
lodash (71KB)
Reason: Full library often imported when only few functions needed
Alternative: lodash-es with tree-shaking or individual imports (lodash/get)
--- IMPORT ISSUES ---
- src/utils/date.ts: Consider replacing moment with date-fns or dayjs
--- RECOMMENDATIONS ---
1. Replace heavy dependencies with lighter alternatives
============================================================- Output Formats: Human-readable report (default) or JSON (
--json).
#!/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()
#!/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()
#!/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 14+ 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"// TODO: Implement {content_key}")
full_path.write_text(content)
created_files.append(str(full_path))
return {
"name": name,
"template": template,
"template_name": template_config["name"],
"features": features,
"path": str(project_path),
"files_created": len(created_files),
"files": created_files,
"next_steps": [
f"cd {name}",
"npm install",
"npm run dev",
],
}
def print_result(result: Dict) -> None:
"""Print scaffolding result."""
if "error" in result:
print(f"Error: {result['error']}", file=sys.stderr)
return
print(f"\n{'='*60}")
print(f"Project Scaffolded: {result['name']}")
print(f"{'='*60}")
print(f"Template: {result['template_name']}")
print(f"Location: {result['path']}")
print(f"Files Created: {result['files_created']}")
if result["features"]:
print(f"Features: {', '.join(result['features'])}")
print(f"\nNext Steps:")
for step in result["next_steps"]:
print(f" $ {step}")
print(f"{'='*60}\n")
def main():
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()
Related skills
FAQ
What level is senior-frontend aimed at?
senior-frontend targets production frontend work: accessibility, performance, component architecture, and design systems. The skill assumes familiarity with modern web frameworks rather than teaching HTML fundamentals.
Does senior-frontend cover state management?
senior-frontend includes state management patterns alongside component architecture and design-system discipline. Use it when structuring complex dashboard or product UIs, not for backend API design.