
Senior Frontend
- 1.3k installs
- 23.5k repo stars
- Updated July 17, 2026
- alirezarezvani/claude-skills
senior-frontend is an agent skill for frontend development skill for react, next.js, typescript, and tailwind css applications. use when building react components, optimizing next.js performance, analyzing bundle.
About
The senior-frontend skill is designed for frontend development skill for React, Next.js, TypeScript, and Tailwind CSS applications. Use when building React components, optimizing Next.js performance, analyzing bundle. Senior Frontend Frontend development patterns, performance optimization, and automation tools for React/Next.js applications. Run the scaffolder with your project name and template: 2. Invoke when the user building React components, optimizing Next.
- Project Scaffolding.
- Component Generation.
- Bundle Analysis.
- React Patterns.
- Next.js Optimization.
Senior Frontend by the numbers
- 1,287 all-time installs (skills.sh)
- +28 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #310 of 2,244 Frontend Development skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
senior-frontend capabilities & compatibility
- Capabilities
- project scaffolding · component generation · bundle analysis · react patterns
What senior-frontend says it does
Frontend development skill for React, Next.js, TypeScript, and Tailwind CSS applications. Use when building React components, optimizing Next.js performance, analyzing bundle sizes
Frontend development skill for React, Next.js, TypeScript, and Tailwind CSS applications. Use when building React components, optimizing Next.js performance, an
npx skills add https://github.com/alirezarezvani/claude-skills --skill senior-frontendAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.3k |
|---|---|
| repo stars | ★ 23.5k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 17, 2026 |
| Repository | alirezarezvani/claude-skills ↗ |
How do I frontend development skill for react, next.js, typescript, and tailwind css applications. use when building react components, optimizing next.js performance, analyzing bundle?
Frontend development skill for React, Next.js, TypeScript, and Tailwind CSS applications. Use when building React components, optimizing Next.js performance, analyzing bundle.
Who is it for?
Developers using senior frontend workflows documented in SKILL.md.
Skip if: Skip when the task falls outside senior-frontend scope or needs a different stack.
When should I use this skill?
User building React components, optimizing Next.
What you get
Completed senior-frontend workflow with documented commands, files, and expected deliverables.
- Framework and rendering strategy
- JS budget constraints
- SEO-critical site architecture plan
By the numbers
- astro-or-static profile version 1.0.0 with read_write_ratio_min 100
- Four ranked framework options: Astro, 11ty, Hugo, and Next.js static export
Files
Senior Frontend
Frontend development patterns, performance optimization, and automation tools for React/Next.js applications.
Table of Contents
- Project Scaffolding
- Component Generation
- Bundle Analysis
- React Patterns
- Next.js Optimization
- Accessibility and Testing
---
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 |
---
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: [{ protocol: 'https', 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>;
}---
Resources
- React Patterns:
references/react_patterns.md - Next.js Optimization:
references/nextjs_optimization_guide.md - Best Practices:
references/frontend_best_practices.md - Forcing-question library (Matt Pocock grill):
references/forcing_questions.md - Composition map (which specialist to fork into):
references/composition_map.md
---
Assumptions and Verifiable Success Criteria (Karpathy discipline)
Before this skill scaffolds a component, recommends a framework, or audits a bundle, the following four assumptions MUST be surfaced.
1. Primary user device + network — mobile-4G, desktop-fiber, low-end-Android, or corporate-network. Drives every perf decision. 2. LCP target in milliseconds — a single number, not "fast." Drives bundle budget and rendering choice. 3. SEO-dependent vs. auth-walled — drives rendering (SSR/SSG/RSC vs. SPA). 4. WCAG target + named a11y owner — AA, AAA, or best-effort. Drives a11y investment and CI gates.
Verifiable success criteria (Karpathy #4) — every recommendation must include:
- Core Web Vitals targets (LCP, INP, CLS) at p75 on the primary device
- A per-route JS bundle budget in KB-gzip
- A Lighthouse a11y floor + perf floor
If any of those three is not stated, the recommendation is incomplete — return to Q2 of the forcing-question library.
The scripts/frontend_decision_engine.py tool encodes these checks: it refuses to recommend a profile without the four assumption inputs and prints the verifiable thresholds for the matched profile.
---
Customization profiles
Four built-in profiles in profiles/ calibrate every recommendation:
| Profile | When to pick | LCP target (mobile-4G p75) | Bundle budget |
|---|---|---|---|
next-app-router | SaaS customer-facing, SEO + dynamic, RSC-first | 2000ms | 150 KB-gzip / route |
remix-or-sveltekit | Mobile-4G primary, low-JS-first, progressive enhancement | 1500ms | 80 KB-gzip / route |
vite-spa | Auth-walled app, desktop/corporate primary | 2500ms | 200 KB init + 80 KB / route |
astro-or-static | Marketing / docs / blog, near-zero write, SEO-critical | 1200ms | 30 KB JS / page |
Pick a profile via:
python scripts/frontend_decision_engine.py \
--primary-device mobile-4g --lcp-target-ms 2000 \
--seo-dependent true --auth-walled false --team-size 5The tool returns the best-fit profile, the runner-up tradeoff (if within 15%), the stack picks, the anti-patterns to avoid on that profile, and the required CI gates.
To add a custom profile (e.g., your org's internal-tool defaults): copy profiles/vite-spa.json to profiles/<your-org>.json and adjust constraints + success_thresholds.
---
Composition map
This skill does NOT reimplement scope owned by the POWERFUL-tier specialists. It forks into them. See references/composition_map.md for the full routing table. Key forks:
| Concern | Fork into |
|---|---|
| WCAG audit, contrast, screen-reader | engineering-team/skills/a11y-audit/ |
| Bundle profiling + runtime perf | engineering/skills/performance-profiler/ |
| Cinematic / scroll-storytelling landing | engineering-team/skills/epic-design/ |
| Apple HIG (iOS / macOS / visionOS) | product-team/skills/apple-hig-expert/ |
| Pre-commit Karpathy review | engineering/karpathy-coder/ |
| Pre-flight architecture grill | engineering/grill-me/ |
The cs-frontend-engineer agent orchestrates these forks via context: fork. Invoke it from another agent with Agent({subagent_type: "cs-frontend-engineer", prompt: "..."}) or via /cs:frontend-review <your problem>.
---
Forcing-question library (Matt Pocock grill)
Before locking any framework or rendering decision, walk the seven forcing questions in references/forcing_questions.md. Discipline:
1. One question per turn. No bundling. 2. Always recommend the answer with cited canon. 3. Track answers in /tmp/frontend-grill-<date>.md. 4. If a kill criterion trips, stop. Don't scaffold around an unresolved gap. 5. After Q7, run frontend_decision_engine.py with the seven answers.
Summary:
1. Primary device + network? 2. LCP target in ms (and INP, CLS)? 3. RSC / SPA / SSR / SSG — pick and defend? 4. JS bundle budget per route? 5. SEO-dependent or auth-walled? 6. Design-system source of truth? 7. WCAG target + named a11y owner?
---
Invocation from other agents and skills
Three surfaces:
1. Slash command: /cs:frontend-review <prompt> — full grill + decision engine + composition routing. 2. Agent subagent: Agent({subagent_type: "cs-frontend-engineer", prompt: "..."}) — forks context, returns ≤ 200-word digest. 3. Direct tool call: python scripts/frontend_decision_engine.py ... — deterministic profile match when inputs are known.
See agents/engineering/cs-frontend-engineer.md for the full invocation contract.
{
"$schema": "https://json-schema.org/draft-07/schema#",
"profile_name": "astro-or-static",
"description": "Content-first marketing / docs / blog / pricing site. Heavy read, near-zero write. SEO-critical. Islands Architecture (Astro) or pure SSG (11ty, Hugo, Next.js static export). Every JS byte must justify itself.",
"version": "1.0.0",
"constraints": {
"primary_device": ["mobile-4g", "desktop-fiber"],
"rendering": "static-generation-with-island-hydration",
"seo_dependent": true,
"auth_walled_only": false,
"team_size_min": 1,
"team_size_max": 5,
"read_write_ratio_min": 100
},
"stack": {
"framework_options_ranked": ["astro", "11ty", "hugo", "next-static-export"],
"language": "typescript-or-markdown-first",
"styling": "tailwind-or-css-modules",
"content": "mdx-in-repo or headless-cms (sanity, contentful, payload)",
"client_js": "islands-only-default-zero",
"image_pipeline": "framework-native (astro:assets, next/image, hugo image processing)",
"forms": "edge-function-to-webhook or formspree",
"analytics": "plausible-or-fathom-or-self-host-umami",
"testing": "playwright-e2e + lighthouse-ci"
},
"anti_recommendations": {
"spa-for-marketing": "kill — SEO catastrophe in 2026 search/AI-search algorithms",
"react-app-on-every-page": "kill — defeats Islands Architecture",
"third-party-tag-soup": "kill — every script adds LCP + INP",
"google-tag-manager-on-marketing": "warn — measure CWV regression before adding",
"custom-cms-build": "kill — use headless CMS or MDX, not a build project",
"next-app-router-for-static-marketing": "warn — RSC complexity tax with no payoff on near-zero-write surface"
},
"success_thresholds": {
"lcp_ms_mobile_4g_p75": 1200,
"inp_ms_p75": 100,
"cls_p75": 0.05,
"ttfb_ms_p75": 250,
"page_weight_kb_total_max": 250,
"js_kb_per_page_gzip_max": 30,
"lighthouse_perf_min": 95,
"lighthouse_a11y_min": 95,
"lighthouse_seo_min": 98,
"lighthouse_best_practices_min": 95
},
"ci_gates": [
"lighthouse-ci-all-four-categories",
"no-broken-links",
"image-budget-per-page",
"axe-a11y-checks"
],
"canon_references": [
"Astro team, Islands Architecture (Eisenberg, 2021) — partial hydration",
"Web Almanac (HTTP Archive, 2025) on marketing-site weight distribution",
"Patrick Stox, JS and SEO (Ahrefs, 2023)",
"Addy Osmani, Web Performance for the Modern Web (2024)",
"Brad Frost, Atomic Design (2016) — content-first layering"
]
}
{
"$schema": "https://json-schema.org/draft-07/schema#",
"profile_name": "next-app-router",
"description": "Next.js 14+ App Router with React Server Components default. Customer-facing SaaS, content-heavy with some personalization, SEO matters. Hybrid SSR/RSC/SSG per route.",
"version": "1.0.0",
"constraints": {
"primary_device": ["mobile-4g", "desktop-fiber"],
"rendering": "rsc-default",
"seo_dependent": true,
"auth_walled_only": false,
"team_size_min": 3,
"team_size_max": 50
},
"stack": {
"framework": "next.js-14+",
"language": "typescript",
"styling": "tailwind-with-design-tokens",
"component_library_options": ["shadcn-ui", "radix-primitives", "ark-ui"],
"state_client": "zustand-or-jotai-for-client-state",
"state_server": "rsc-with-server-actions",
"data_fetching": "rsc-async-components + react-query-for-client-only",
"forms": "react-hook-form + zod",
"testing": "vitest + react-testing-library + playwright-e2e",
"icons": "lucide-react",
"fonts": "next-font-google-or-self-hosted"
},
"anti_recommendations": {
"use-client-everywhere": "kill — defeats the RSC value; reserve 'use client' for actual interactivity",
"global-state-in-context-everywhere": "kill — props down + server state up; Context is for tree-scoped state only",
"csr-only-on-seo-routes": "kill — RSC or SSR on routes that matter for SEO",
"redux-without-justification": "warn — RTK is fine if you've shipped it; new project should default to Zustand/Jotai",
"css-in-js-runtime": "kill — styled-components runtime mode breaks RSC; use tailwind or zero-runtime alternatives",
"default-imports-for-icons": "kill — tree-shake hostile; use named imports from lucide"
},
"success_thresholds": {
"lcp_ms_mobile_4g_p75": 2000,
"inp_ms_p75": 150,
"cls_p75": 0.05,
"bundle_kb_gzip_per_route_max": 150,
"framework_overhead_kb_gzip_max": 90,
"lighthouse_perf_min": 85,
"lighthouse_a11y_min": 95,
"lighthouse_seo_min": 95,
"test_coverage_min": 0.6
},
"ci_gates": [
"bundlewatch-per-route",
"lighthouse-ci",
"a11y-axe-checks",
"playwright-smoke",
"typecheck-strict"
],
"canon_references": [
"Dan Abramov, RSC spec (Vercel, 2023)",
"Web Almanac (HTTP Archive, 2025) on Next.js distribution",
"Tim Kadlec, Performance Budgets (2013)",
"Brad Frost, Atomic Design (2016) — for design system layering",
"shadcn/ui project (2023+) — copy-paste components"
]
}
{
"$schema": "https://json-schema.org/draft-07/schema#",
"profile_name": "remix-or-sveltekit",
"description": "Server-rendered framework with progressive enhancement (Remix v2 / SvelteKit). Low-JS-first. SEO-dependent. Mobile-4G or low-end Android primary device. Teams that find RSC complexity tax not worth it.",
"version": "1.0.0",
"constraints": {
"primary_device": ["mobile-4g", "low-end-android"],
"rendering": "server-rendered-progressive-enhancement",
"seo_dependent": true,
"auth_walled_only": false,
"team_size_min": 2,
"team_size_max": 30
},
"stack": {
"framework_options": ["remix-v2", "sveltekit"],
"language": "typescript",
"styling": "tailwind-or-vanilla-extract",
"component_pattern": "platform-first-html-then-enhance",
"state": "url-query-params-and-cookies-as-state",
"data_fetching": "framework-loaders-and-actions",
"forms": "platform-form-with-progressive-enhancement-not-react-hook-form",
"testing": "vitest + playwright-e2e"
},
"anti_recommendations": {
"swr-or-react-query": "kill — duplicates framework loaders; pick one",
"client-router-on-top": "kill — defeats the framework's purpose",
"heavy-client-state-libs": "warn — Remix/SvelteKit philosophy is URL-as-state; avoid Zustand/Jotai unless justified",
"rsc-style-data-flow": "kill — wrong framework if you want RSC; switch to Next App Router",
"no-progressive-enhancement": "kill — defeats the framework's value prop"
},
"success_thresholds": {
"lcp_ms_mobile_4g_p75": 1500,
"inp_ms_p75": 100,
"cls_p75": 0.05,
"bundle_kb_gzip_per_route_max": 80,
"framework_overhead_kb_gzip_max": 40,
"lighthouse_perf_min": 90,
"lighthouse_a11y_min": 95,
"lighthouse_seo_min": 95,
"test_coverage_min": 0.6,
"javascript_disabled_works": true
},
"ci_gates": [
"bundlewatch-per-route",
"lighthouse-ci",
"a11y-axe-checks",
"no-js-smoke-test-can-submit-forms"
],
"canon_references": [
"Ryan Florence + Michael Jackson, Remix data-loading philosophy (2021-2024)",
"Rich Harris, Frameworks Without Hydration (2023+, SvelteKit/Svelte 5)",
"Jeremy Keith, Resilient Web Design (2016) — progressive enhancement",
"Alex Russell, The Performance Inequality Gap (2021-2024)",
"Tim Berners-Lee, principles of the web (1989-1990) — content-first"
]
}
{
"$schema": "https://json-schema.org/draft-07/schema#",
"profile_name": "vite-spa",
"description": "Vite + React (or Vue/Solid) SPA for an auth-walled application. Login → app shell loads once → client routing. No SEO. Heavier JS bundle is acceptable because users come back. Internal tools, dashboards, B2B apps.",
"version": "1.0.0",
"constraints": {
"primary_device": ["desktop-fiber", "corporate-network"],
"rendering": "spa",
"seo_dependent": false,
"auth_walled_only": true,
"team_size_min": 1,
"team_size_max": 15
},
"stack": {
"build_tool": "vite",
"framework_options": ["react", "vue", "solid", "preact"],
"language": "typescript",
"styling": "tailwind-with-css-modules-for-component-isolation",
"component_library_options": ["shadcn-ui", "mantine", "chakra-ui", "ant-design"],
"router": "react-router-v6 or tanstack-router",
"state_client": "zustand-or-jotai or redux-toolkit",
"data_fetching": "tanstack-query (react-query)",
"forms": "react-hook-form + zod",
"testing": "vitest + react-testing-library + playwright-e2e",
"code_split": "route-level-lazy-load-mandatory"
},
"anti_recommendations": {
"no-code-splitting": "kill — single bundle for a multi-route SPA = unusable on slow networks",
"ssr-on-spa-only-surface": "warn — adds infra cost with no SEO benefit",
"next-or-remix-for-pure-spa": "warn — overkill; Vite is leaner",
"redux-without-justification": "warn — TanStack Query handles server state; Zustand handles UI state",
"context-as-global-state": "kill — re-renders cascade; use Zustand/Jotai for global UI state"
},
"success_thresholds": {
"lcp_ms_corporate_network_p75": 2500,
"inp_ms_p75": 200,
"cls_p75": 0.1,
"initial_bundle_kb_gzip_max": 200,
"per_route_chunk_kb_gzip_max": 80,
"lighthouse_perf_min": 80,
"lighthouse_a11y_min": 90,
"test_coverage_min": 0.5
},
"ci_gates": [
"bundlewatch-initial-and-per-route",
"a11y-axe-checks",
"playwright-smoke-on-key-flows",
"typecheck-strict"
],
"canon_references": [
"Evan You + Vite team, Vite docs (2020-2024)",
"Brad Frost, Atomic Design (2016)",
"TanStack Query docs — server state vs UI state distinction",
"Marcy Sutton, Accessibility in JavaScript Applications (2017+)"
]
}
Frontend Engineer — Composition Map
Principle (Karpathy #2, Simplicity First): do not reimplement scope that the POWERFUL-tier specialists already own. This skill is the frontend orchestrator; the specialists are the implementers.
This map is the routing table for the cs-frontend-engineer agent and the /cs:frontend-review command.
Composition routing table
| User concern | Fork into | When to fork | Path |
|---|---|---|---|
| WCAG audit, contrast checks, screen-reader gaps | a11y-audit | After Q7 (WCAG target) is set | ../../../engineering-team/skills/a11y-audit/ |
| Bundle profiling, Lighthouse perf, runtime CPU/memory | performance-profiler | After Q2 (LCP target) is set | ../../../engineering/skills/performance-profiler/ |
| Cinematic / parallax / scroll-storytelling landing | epic-design | When marketing-site or landing-page profile applies | ../../../engineering-team/skills/epic-design/ |
| Pre-commit Karpathy review on changed files | cs-karpathy-reviewer | Before EVERY commit this skill produces | ../../../engineering/karpathy-coder/ |
| Pre-flight architecture grill | cs-grill-master | Before locking framework or rendering model | ../../../engineering/grill-me/ |
| Monorepo coordination (Turbo / Nx / pnpm) | monorepo-navigator | When frontend shares repo with backend / mobile / extension | ../../../engineering/skills/monorepo-navigator/ |
| Dependency vulnerability sweep | dependency-auditor | Before every major release | ../../../engineering/skills/dependency-auditor/ |
| Visual / accessibility regression in CI | api-test-suite-builder (extend for visual) + playwright-pro | After Q7 (WCAG target) is set | ../../../engineering-team/playwright-pro/ |
| Apple HIG / iOS / macOS / visionOS app review | apple-hig-expert | When the surface is Apple-platform-native | ../../../product-team/skills/apple-hig-expert/ |
| AEO (Answer Engine Optimization) — visibility in LLM search | aeo | After Q5 (SEO-dependent surface) is confirmed | ../../../marketing-skill/skills/aeo/ |
| SEO crawlability + meta + structured data | seo-auditor (if present) | After Q5 (SEO-dependent surface) | search skills/ for the SEO auditor entry point |
| API contract from the consumer side | api-design-reviewer | When frontend defines/consumes a new API contract | ../../../engineering/skills/api-design-reviewer/ |
Composition rules
1. Fork via `context: fork` — the agent forks its own context, runs the sub-skill, returns a ≤ 200-word digest. 2. One sub-skill at a time. Matt Pocock's depth-first rule. Finish the a11y branch before opening the perf branch. 3. Honor sub-skill outputs as inputs. performance-profiler produces a baseline; the next iteration of senior-frontend must respect that baseline. 4. Never reimplement specialist scope. If the user asks "what's my CLS?" do not hand-roll a check — fork into performance-profiler. 5. Document the chain. Every artifact lists the sub-skills invoked, in order.
Anti-patterns
- ❌ Adding a third-party perf monitoring lib without checking it against the bundle budget from Q4.
- ❌ Implementing what
a11y-auditwould have caught (e.g., missing alt text, color contrast, focus traps). - ❌ Skipping
cs-karpathy-reviewerbefore committing — every commit must pass the diff-noise gate. - ❌ Treating shipped UI as a final product without
playwright-provisual-regression baseline.
When to escalate out of frontend
- Brand voice / copy → escalate to
marketing-skill/content-creator+cs-content-creatoragent. - Backend API design → escalate to
cs-backend-engineer+api-design-reviewer. - iOS/macOS-native UI → escalate to
cs-apple-hig(if present) orproduct-team/skills/apple-hig-expert. - Marketing-site infrastructure choice (Astro vs Next vs Hugo) → escalate to
cs-fullstack-engineer(marketing-site profile).
References
- Karpathy 4 principles →
../../../engineering/karpathy-coder/skills/karpathy-coder/references/karpathy-principles.md - Matt Pocock grill discipline →
../../../engineering/grill-me/skills/grill-me/references/forcing_question_patterns.md - Path-B 11-file contract →
../../../business-operations/CLAUDE.md
Frontend Engineer — Forcing-Question Library
Discipline (Matt Pocock, derived from `engineering/grill-me`, MIT): walk these one at a time. Do not skip ahead. Do not bundle. Answers must be written down. If the user cannot answer one, that is your next investigation — stop and surface the gap.
These seven questions gate every meaningful frontend decision: framework pick, rendering model, bundle budget, design-system investment, a11y target. Each has a recommended answer with canon citation and a kill criterion.
---
Q1 — "Primary user device + network condition: desktop-fiber, mobile-4G, low-end Android, or corporate-network?"
Recommended answer: one named segment with evidence (analytics breakdown, target market, deployment context). Not "all of them" — every frontend optimizes for one and tolerates the others.
Why it's the first question: every rendering / bundling / image-pipeline decision changes shape based on the network floor. A mobile-4G product CANNOT ship a 500KB JS bundle; a corporate-internal tool on fiber CAN. Optimizing for the wrong target is the #1 frontend cost overrun.
Kill criterion: "all users equally" — STOP. Pull analytics or target-market data. The frontend tax for an unknown floor is paid by every user.
Canon: Web Almanac (HTTP Archive, 2025) — device + network distribution; Addy Osmani, Web Performance for the Modern Web (2024); Tim Kadlec, High Performance Images (2016).
---
Q2 — "What is your LCP target on the primary device? Pick a number in milliseconds."
Recommended answer: a single number (e.g., "LCP < 2.0s on mobile-4G p75"). Bonus for naming the p75 / p95 split.
Why it matters: Core Web Vitals are a Google ranking signal and a measured business metric (every 100ms of LCP improvement = ~1% conversion lift per Akamai 2017 and reaffirmed by Chrome UX Report 2024). "Fast" is not a target. The number gates the entire performance-investment conversation.
Kill criterion: "as fast as possible" — STOP. Pick a number. Without a target there's no way to know when to stop optimizing.
Canon: Chrome UX Report (CrUX) public dataset; Akamai Online Retail Performance (2017); Google Web Vitals spec (web.dev/vitals, 2020–2024).
Related targets to set in the same turn: INP < 200ms; CLS < 0.1. If the user has not heard of INP, walk them through the 2024 migration from FID → INP (Google, March 2024).
---
Q3 — "Server Components (RSC), classic SPA, server-rendered (SSR), or static (SSG)? Pick and defend."
Recommended answer: one of the four with explicit rationale tied to Q1 (network) and Q2 (LCP). Defaults: marketing → SSG; SEO-dependent + dynamic → SSR or RSC; auth-walled app → SPA; content-heavy with personalization → RSC.
Why it matters: the rendering choice cascades into every other decision (data fetching, state management, hydration cost, server cost). Picking it implicitly (= "whatever the framework default is") locks in costs the team won't notice until production traffic shows up.
Kill criterion: "RSC because it's newest" with no LCP measurement on a comparable SSR baseline — STOP. RSC is not faster by default; it's faster for some workloads and slower for others. Measure.
Canon: Dan Abramov, React Server Components spec (Vercel, 2023); Ryan Florence, Remix data-loading patterns (2022–2024); Astro Islands Architecture (Eisenberg, 2021); Rich Harris, Frameworks Without Hydration (Svelte 5, 2024).
---
Q4 — "What is the JS bundle budget per route in KB (gzipped)?"
Recommended answer: a per-route number (e.g., "< 80KB gzip for landing, < 150KB gzip for app routes, hard cap at 200KB"). Bonus: split between framework + app + third-party.
Why it matters: the bundle budget is the only thing that holds the team accountable. Without a number, every new feature adds 5–20KB; in 12 months the app is 800KB and Q2's LCP target is impossible. Tim Kadlec's Performance Budgets (2013) framing — set the ceiling, fail the build when it's crossed.
Kill criterion: no per-route budget set in CI — STOP. Add bundlewatch or size-limit to CI with a failing gate before shipping the next feature.
Canon: Tim Kadlec, Performance Budgets (2013); Patrick Stox, JavaScript and SEO (Ahrefs, 2023); Alex Russell, The Performance Inequality Gap (2021–2024).
---
Q5 — "Is the surface SEO-dependent or auth-walled?"
Recommended answer: one of the two, written down. "Both" means split the surface — public marketing pages go static / SSR; auth-walled app goes SPA or SSR-with-no-SEO-investment.
Why it matters: an SEO-dependent surface MUST render content in HTML (not just JS) and MUST optimize for Core Web Vitals (ranking signal). An auth-walled surface CAN ship a heavier JS bundle (no SEO cost) and CAN defer SSR. Picking the wrong rendering for the wrong surface is a common pre-Series-A waste.
Kill criterion: SEO-dependent + SPA-only rendering — STOP. Switch to SSR/SSG/RSC, or accept the SEO penalty in writing (signed by marketing-lead).
Canon: Google Search Quality Rater Guidelines (2024); Patrick Stox, JS-rendered pages and crawl budget (Ahrefs, 2023); John Mueller (Google) on JS-rendering best practices (2020–2024 SearchOff Hours).
---
Q6 — "Where does your design system live: Figma + tokens, ad-hoc Tailwind, or a headless UI library?"
Recommended answer: one of the three with a named owner. Bonus: name the token export path (e.g., tokens.json synced via Style Dictionary).
Why it matters: ad-hoc styling at team size > 3 produces a fork-bomb (5 button variants, 9 modal stylings, 17 spacing values). A design system isn't a luxury — it's the only way to keep the visual language consistent past 3 engineers. But a custom design system at team size ≤ 3 is a tar pit; use shadcn/ui + Tailwind tokens instead.
Kill criterion: team size ≥ 4 with no design-system source of truth — STOP. Pick: Figma + Style Dictionary, or shadcn/ui + Tailwind, or a headless library (Radix, Ark, React Aria). No fourth option.
Canon: Brad Frost, Atomic Design (2016); Nathan Curtis, Design Systems Handbook (InVision, 2017); Vitaly Friedman, Design Systems by Smashing (2020–2024); shadcn/ui project (2023–2024) on copy-paste components vs. library lock-in.
---
Q7 — "WCAG target — AA, AAA, or best-effort? And who is the accessibility owner?"
Recommended answer: one of WCAG 2.2 AA (the legal default in EU/AU/CA/many US states), 2.2 AAA (rare; public-sector or accessibility-first product), or best-effort (auth-walled internal-only). PLUS a named owner.
Why it matters: a11y is a regulatory baseline in 2026 (European Accessibility Act enforcement began 2025; US ADA Title III litigation surged 2018–2024). "We'll fix it later" is the most expensive a11y strategy — retrofitting costs 5–10× building it in. AND without a named owner, no one is accountable.
Kill criterion: customer-facing surface + no named a11y owner — STOP. Assign one before scaffolding. Run engineering-team/skills/a11y-audit as part of CI.
Canon: W3C WCAG 2.2 (2023); Marcy Sutton, Accessibility in JavaScript Applications (2017+); Adrian Roselli's blog on a11y testing (a-roselli.com, 2015–2024); European Accessibility Act (EU 2019/882, enforced 2025).
---
How to use this library in a conversation
1. State the rule first — tell the user you'll walk seven questions, one at a time, before recommending any framework or rendering model. 2. One question per turn. Never bundle. 3. Recommend the answer. Always cite the canon source for why this is the right shape. 4. Surface the kill criterion. If the user's answer trips it, stop and surface that gap. Do not proceed. 5. Track the answers. Write them to a working file (e.g., /tmp/frontend-grill-<date>.md). 6. After Q7, recommend the profile. Match the seven answers against the profile JSON files in ../profiles/ and pick the closest fit.
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]
);#!/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_decision_engine.py — Deterministic frontend framework + rendering picker.
Stdlib-only. No LLM calls. Same input -> same output. Matches caller-supplied
constraints (primary device, LCP target, SEO-dependence, auth-walled, team
size) against profile JSON files in ../profiles/ and returns a ranked
recommendation with bundle budget, anti-patterns, and verifiable success
thresholds.
Karpathy discipline:
- #1 Think Before Coding: requires --primary-device, --lcp-target-ms,
--seo-dependent, --auth-walled, --team-size.
- #4 Goal-Driven Execution: every recommendation prints the bundle and
Web Vitals thresholds the chosen profile commits to.
Usage:
python frontend_decision_engine.py --help
python frontend_decision_engine.py --sample
python frontend_decision_engine.py \\
--primary-device mobile-4g --lcp-target-ms 2000 \\
--seo-dependent true --auth-walled false --team-size 5
python frontend_decision_engine.py ... --output json
python frontend_decision_engine.py --list-profiles
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Any
SCRIPT_DIR = Path(__file__).resolve().parent
PROFILES_DIR = SCRIPT_DIR.parent / "profiles"
@dataclass
class Inputs:
primary_device: str
lcp_target_ms: int
seo_dependent: bool
auth_walled: bool
team_size: int
read_write_ratio: float
inp_target_ms: int
def kill_criteria_check(self) -> list[str]:
kills: list[str] = []
if self.seo_dependent and self.auth_walled:
kills.append(
"seo-dependent AND auth-walled: split the surface — public marketing "
"goes static/SSR; auth-walled app goes SPA. Do not pick a single profile for both."
)
if self.primary_device == "mobile-4g" and self.lcp_target_ms > 3000:
kills.append(
f"mobile-4g primary with LCP target {self.lcp_target_ms}ms: "
"target is too loose for the device class. Tighten to < 2500ms (Web Vitals 'good')."
)
if self.primary_device == "mobile-4g" and self.inp_target_ms > 300:
kills.append(
f"mobile-4g primary with INP target {self.inp_target_ms}ms: "
"target is too loose for the device class. Tighten to < 200ms (Web Vitals 'good')."
)
if self.team_size < 1:
kills.append("team_size < 1 makes no sense.")
return kills
@dataclass
class Match:
profile_name: str
score: float
matched_constraints: list[str] = field(default_factory=list)
violated_constraints: list[str] = field(default_factory=list)
profile_data: dict[str, Any] = field(default_factory=dict)
def load_profiles() -> dict[str, dict[str, Any]]:
profiles: dict[str, dict[str, Any]] = {}
if not PROFILES_DIR.exists():
return profiles
for p in sorted(PROFILES_DIR.glob("*.json")):
with p.open() as f:
data = json.load(f)
profiles[data.get("profile_name", p.stem)] = data
return profiles
def score_profile(profile: dict[str, Any], inputs: Inputs) -> Match:
name = profile.get("profile_name", "unknown")
c = profile.get("constraints", {})
matched: list[str] = []
violated: list[str] = []
w_total = 0.0
w_matched = 0.0
def check(label: str, ok: bool, weight: float) -> None:
nonlocal w_total, w_matched
w_total += weight
if ok:
w_matched += weight
matched.append(label)
else:
violated.append(label)
if "primary_device" in c:
devices = c["primary_device"] if isinstance(c["primary_device"], list) else [c["primary_device"]]
check(f"primary_device in {devices}", inputs.primary_device in devices, weight=2.0)
if "seo_dependent" in c:
check(f"seo_dependent = {c['seo_dependent']}", inputs.seo_dependent == c["seo_dependent"], weight=2.0)
if "auth_walled_only" in c:
check(f"auth_walled_only = {c['auth_walled_only']}", inputs.auth_walled == c["auth_walled_only"], weight=2.0)
if "team_size_min" in c:
check(f"team_size >= {c['team_size_min']}", inputs.team_size >= c["team_size_min"], weight=1.0)
if "team_size_max" in c:
check(f"team_size <= {c['team_size_max']}", inputs.team_size <= c["team_size_max"], weight=1.0)
if "read_write_ratio_min" in c:
check(f"read_write_ratio >= {c['read_write_ratio_min']}", inputs.read_write_ratio >= c["read_write_ratio_min"], weight=1.0)
thresholds = profile.get("success_thresholds", {})
if "lcp_ms_mobile_4g_p75" in thresholds:
check(
f"lcp_target supports {thresholds['lcp_ms_mobile_4g_p75']}ms p75",
inputs.lcp_target_ms >= thresholds["lcp_ms_mobile_4g_p75"],
weight=1.0,
)
score = w_matched / w_total if w_total > 0 else 0.0
return Match(
profile_name=name,
score=score,
matched_constraints=matched,
violated_constraints=violated,
profile_data=profile,
)
def rank(profiles: dict[str, dict[str, Any]], inputs: Inputs) -> list[Match]:
matches = [score_profile(p, inputs) for p in profiles.values()]
matches.sort(key=lambda m: m.score, reverse=True)
return matches
def render_markdown(inputs: Inputs, matches: list[Match], kills: list[str]) -> str:
L: list[str] = []
L.append("# Frontend Stack Decision")
L.append("")
L.append("## Inputs (your assumptions, Karpathy #1)")
L.append("")
for k, v in asdict(inputs).items():
L.append(f"- **{k}**: `{v}`")
L.append("")
if kills:
L.append("## Kill criteria tripped — STOP and resolve")
L.append("")
for k in kills:
L.append(f"- {k}")
L.append("")
if not matches:
L.append("No profiles found in ../profiles/.")
return "\n".join(L)
top = matches[0]
second = matches[1] if len(matches) > 1 else None
L.append("## Recommended profile")
L.append("")
L.append(f"**{top.profile_name}** — fit score {top.score:.0%}")
L.append("")
L.append(f"_{top.profile_data.get('description', '')}_")
L.append("")
if top.matched_constraints:
L.append("**Matched:**")
for c in top.matched_constraints:
L.append(f"- {c}")
L.append("")
if top.violated_constraints:
L.append("**Violated (review before locking):**")
for c in top.violated_constraints:
L.append(f"- {c}")
L.append("")
if second and abs(top.score - second.score) < 0.15:
L.append(f"## Close runner-up: {second.profile_name} ({second.score:.0%}) — surface the tradeoff.")
L.append("")
stack = top.profile_data.get("stack", {})
if stack:
L.append("## Stack")
L.append("")
L.append("```json")
L.append(json.dumps(stack, indent=2))
L.append("```")
L.append("")
anti = top.profile_data.get("anti_recommendations", {})
if anti:
L.append("## Anti-patterns (DO NOT introduce on this profile)")
L.append("")
for k, v in anti.items():
L.append(f"- **{k}** — {v}")
L.append("")
thresh = top.profile_data.get("success_thresholds", {})
if thresh:
L.append("## Verifiable success criteria (Karpathy #4)")
L.append("")
for k, v in thresh.items():
L.append(f"- `{k}` = {v}")
L.append("")
gates = top.profile_data.get("ci_gates", [])
if gates:
L.append("## CI gates (required)")
L.append("")
for g in gates:
L.append(f"- {g}")
L.append("")
canon = top.profile_data.get("canon_references", [])
if canon:
L.append("## Canon")
L.append("")
for c in canon:
L.append(f"- {c}")
L.append("")
L.append("---")
L.append("")
L.append("Walk `references/forcing_questions.md` BEFORE scaffolding. Do not pick this profile silently.")
return "\n".join(L)
def render_json(inputs: Inputs, matches: list[Match], kills: list[str]) -> str:
return json.dumps(
{
"inputs": asdict(inputs),
"kill_criteria_tripped": kills,
"ranked_matches": [
{
"profile_name": m.profile_name,
"score": round(m.score, 4),
"matched_constraints": m.matched_constraints,
"violated_constraints": m.violated_constraints,
"stack": m.profile_data.get("stack", {}),
"anti_recommendations": m.profile_data.get("anti_recommendations", {}),
"success_thresholds": m.profile_data.get("success_thresholds", {}),
"ci_gates": m.profile_data.get("ci_gates", []),
}
for m in matches
],
},
indent=2,
)
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
description="Deterministic frontend framework + rendering picker. Surfaces tradeoffs + bundle budget + anti-patterns. Never auto-approves.",
epilog="See ../references/forcing_questions.md for the 7-question grill.",
)
p.add_argument(
"--primary-device",
choices=["mobile-4g", "desktop-fiber", "low-end-android", "corporate-network"],
help="Primary device + network condition.",
)
p.add_argument("--lcp-target-ms", type=int, help="LCP target in milliseconds (p75 on primary device).")
p.add_argument("--inp-target-ms", type=int, default=200, help="INP target in milliseconds (default 200).")
p.add_argument("--seo-dependent", choices=["true", "false"], help="Is the surface SEO-dependent?")
p.add_argument("--auth-walled", choices=["true", "false"], help="Is the surface fully auth-walled?")
p.add_argument("--team-size", type=int, help="Frontend engineers on this surface.")
p.add_argument("--read-write-ratio", type=float, default=1.0, help="Reads per write (>= 100 hints static).")
p.add_argument("--output", choices=["markdown", "json"], default="markdown")
p.add_argument("--list-profiles", action="store_true")
p.add_argument("--sample", action="store_true")
return p
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
profiles = load_profiles()
if args.list_profiles:
if not profiles:
print("No profiles found in", PROFILES_DIR, file=sys.stderr)
return 1
for name, data in profiles.items():
print(f"{name}: {data.get('description', '')[:120]}")
return 0
if args.sample:
inputs = Inputs(
primary_device="mobile-4g",
lcp_target_ms=2000,
seo_dependent=True,
auth_walled=False,
team_size=5,
read_write_ratio=4.0,
inp_target_ms=150,
)
else:
required = [
("primary_device", args.primary_device),
("lcp_target_ms", args.lcp_target_ms),
("seo_dependent", args.seo_dependent),
("auth_walled", args.auth_walled),
("team_size", args.team_size),
]
missing = [n for n, v in required if v is None]
if missing:
print("Missing required inputs: " + ", ".join(missing), file=sys.stderr)
print("Run with --sample for an example, or --list-profiles.", file=sys.stderr)
return 2
inputs = Inputs(
primary_device=args.primary_device,
lcp_target_ms=args.lcp_target_ms,
seo_dependent=(args.seo_dependent == "true"),
auth_walled=(args.auth_walled == "true"),
team_size=args.team_size,
read_write_ratio=args.read_write_ratio,
inp_target_ms=args.inp_target_ms,
)
kills = inputs.kill_criteria_check()
matches = rank(profiles, inputs)
if args.output == "json":
print(render_json(inputs, matches, kills))
else:
print(render_markdown(inputs, matches, kills))
return 0
if __name__ == "__main__":
sys.exit(main())
Related skills
How it compares
Pick senior-frontend over general React skills when the site is content-first, SEO-dependent, and should default to SSG or islands rather than client-heavy SPAs.
FAQ
What does senior-frontend do?
Frontend development skill for React, Next.js, TypeScript, and Tailwind CSS applications. Use when building React components, optimizing Next.js performance, analyzing bundle.
When should I use senior-frontend?
User building React components, optimizing Next.
Is senior-frontend safe to install?
Review the Security Audits panel on this page before installing in production.