Senior Frontend
Frontend development patterns, performance optimization, and automation tools for React/Next.js applications.
This skill favors a small set of opinionated defaults — TypeScript in strict mode, Tailwind design tokens, tree-shakeable dependencies, and accessibility baked in — because the cost of these decisions is paid once at the start, while the cost of retrofitting them grows with every file added. The scripts under scripts/ exist to make the correct path the fast path: scaffolding a project, generating a component, or auditing a bundle should take seconds and produce a result you do not have to clean up afterward.
When to Use
- Scaffolding a new React or Next.js app. A generated baseline gives every project the same
stricttsconfig, lint/format rules, and Tailwind tokens. That consistency removes per-project config bikeshedding and means a reviewer can move between repos without relearning conventions. - Generating components or custom hooks. The generator enforces one predictable file layout (component + optional test + story + barrel export). Predictable structure is what lets both humans and tooling (codemods, import resolvers) reason about the codebase reliably.
- Analyzing and optimizing bundle size. Bundle weight is the single largest lever on first-load performance and Core Web Vitals. The analyzer catches heavy dependencies and import anti-patterns before they reach users, when fixing them is cheap.
- Implementing advanced React patterns such as Compound Components or custom hooks. These patterns keep complex, stateful UI composable and accessible without prop-drilling — reach for them when a widget has several interdependent parts, not for simple leaf components where they add indirection for no gain.
- Ensuring accessibility and testing. Accessibility and tests are dramatically cheaper to build in from the first commit than to bolt on later. Treat a missing label or an untested interaction as a defect, not a follow-up.
When this skill is the wrong tool
- Backend or server-side data-layer work. Database access, auth issuance, and infrastructure are different concerns with different failure modes. The generators here assume the React/Next client surface, so use a backend-focused skill for those layers.
- Reaching for heavyweight legacy libraries like
momentor a fulllodashimport. They ship large, often unused code and carry ongoing maintenance and security risk. Preferdate-fns/dayjsfor dates and native ES methods orlodash-es(with tree-shaking) for utilities. The exception is a codebase already standardized on one — consistency can outweigh a few KB until a planned migration. - Shipping Next.js experimental features to production unguarded. Experimental APIs can change or break between minor releases. Use them only behind a clear upgrade plan and with a fallback, so an upstream change does not become an incident.
- Skipping accessibility to move faster. Semantic HTML and correct ARIA are part of "done," not an enhancement. Broken keyboard navigation or missing labels exclude real users and are treated as bugs.
Prerequisites
- Python 3.8+ installed and on
PATH— all scripts underscripts/are Python-based. - Node.js 18+ and npm — required for installing dependencies and running the dev server.
- Windows host (PowerShell) is the primary environment. Use backslash paths or forward-slash paths consistently within a single command. On Windows PowerShell, use
python(notpython3).
Procedure
1. Project Scaffolding
Generate a new Next.js or React project with TypeScript, Tailwind CSS, and best-practice configurations.
Run the scaffolder with your project name and template. The template choice (
nextjsvsreact) determines the routing model and build tool — picknextjsfor server components and file-based routing,reactfor a lean SPA (React + Vite):python scripts/frontend_scaffolder.py my-app --template nextjsAdd optional features only as you need them. Valid features are
auth,api,forms,testing, andstorybook:python scripts/frontend_scaffolder.py dashboard --template nextjs --features auth,apiInspect what a feature brings in before committing:
python scripts/frontend_scaffolder.py dashboard --list-featuresPreview the full file tree without writing to disk (dry run):
python scripts/frontend_scaffolder.py dashboard --template nextjs --features auth,api --dry-runNavigate to the project and install dependencies:
cd my-app; npm installStart the development server and confirm the home page renders before building on top of it:
npm run dev
2. Component Generation
Generate React components with TypeScript, tests, and Storybook stories.
Generate a client component when the UI needs browser-only APIs, state, or event handlers (
'use client'is added automatically):python scripts/component_generator.py Button --dir src/components/uiGenerate a server component when the UI only renders data and needs no interactivity — server components ship zero client JS, the cheaper default in Next.js:
python scripts/component_generator.py ProductCard --type serverGenerate with test and story files when the component has behavior or visual states worth documenting:
python scripts/component_generator.py UserProfile --with-test --with-storyGenerate a custom hook to extract reusable, stateful logic out of components:
python scripts/component_generator.py FormValidation --type hookPreview files with
--dry-runbefore anything is written:python scripts/component_generator.py UserProfile --with-test --with-story --dry-run
3. Bundle Analysis
Analyze package.json and project structure for bundle optimization opportunities. Run this regularly — regressions creep in one dependency at a time.
Run the analyzer on your project (positional argument is the project directory to scan):
python scripts/bundle_analyzer.py /path/to/projectReview the health score and issues. A high grade means no urgent action; flagged "heavy dependencies" point to the changes with the biggest payoff. Each entry pairs the cost with a lighter alternative:
Bundle Health Score: 75/100 (C) HEAVY DEPENDENCIES: moment (290KB) Alternative: date-fns (12KB) or dayjs (2KB) lodash (71KB) Alternative: lodash-es with tree-shakingApply recommended fixes by replacing heavy dependencies. Prioritize by size saved versus migration effort — swapping
momentfordate-fnsremoves ~280KB and is usually mechanical.Re-run with verbose mode to inspect import patterns (barrel/whole-library imports that defeat tree-shaking):
python scripts/bundle_analyzer.py . --verboseFor CI gating or scripted checks, request machine-readable output:
python scripts/bundle_analyzer.py . --json
4. Advanced React Patterns
Compound Components
Use compound components when several parts of a widget must share state but you still want a flexible, declarative markup API. The pattern keeps shared state in a context so the parts coordinate without the parent wiring props through every level.
Key defensive choices in the implementation below:
- Context defaults to
null(so "used outside the provider" is detectable). - A single reader hook throws a precise error naming the exact component.
- The markup carries the ARIA wiring a tablist needs.
import {
createContext,
useContext,
useId,
useMemo,
useState,
type ReactNode,
} from 'react';
import { cn } from '@/lib/utils';
interface TabsContextValue {
activeTab: string;
selectTab: (id: string) => void;
baseId: string;
}
const TabsContext = createContext<TabsContextValue | null>(null);
function useTabsContext(componentName: string): TabsContextValue {
const context = useContext(TabsContext);
if (context === null) {
throw new Error(
`<Tabs.${componentName}> must be rendered inside a <Tabs> provider.`
);
}
return context;
}
interface TabsProps {
defaultTab: string;
children: ReactNode;
}
export function Tabs({ defaultTab, children }: TabsProps): JSX.Element {
if (!defaultTab) {
throw new Error('<Tabs> requires a non-empty `defaultTab` id.');
}
const baseId = useId();
const [activeTab, setActiveTab] = useState<string>(defaultTab);
const value = useMemo<TabsContextValue>(
() => ({ activeTab, selectTab: setActiveTab, baseId }),
[activeTab, baseId]
);
return <TabsContext.Provider value={value}>{children}</TabsContext.Provider>;
}
interface TabsListProps {
children: ReactNode;
'aria-label': string;
}
function TabsList({ children, 'aria-label': ariaLabel }: TabsListProps): JSX.Element {
return (
<div role="tablist" aria-label={ariaLabel} className="flex gap-1 border-b">
{children}
</div>
);
}
interface TabProps {
id: string;
children: ReactNode;
}
function Tab({ id, children }: TabProps): JSX.Element {
const { activeTab, selectTab, baseId } = useTabsContext('Tab');
const isActive = activeTab === id;
return (
<button
type="button"
role="tab"
id={`${baseId}-tab-${id}`}
aria-selected={isActive}
aria-controls={`${baseId}-panel-${id}`}
tabIndex={isActive ? 0 : -1}
=> selectTab(id)}
className={cn(
'px-4 py-2 text-sm font-medium transition-colors',
isActive
? 'border-b-2 border-primary text-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
>
{children}
</button>
);
}
interface TabPanelProps {
id: string;
children: ReactNode;
}
function TabPanel({ id, children }: TabPanelProps): JSX.Element | null {
const { activeTab, baseId } = useTabsContext('Panel');
if (activeTab !== id) {
return null;
}
return (
<div
role="tabpanel"
id={`${baseId}-panel-${id}`}
aria-labelledby={`${baseId}-tab-${id}`}
tabIndex={0}
className="p-4"
>
{children}
</div>
);
}
Tabs.List = TabsList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
Usage — ids tie each Tab to its Panel, and aria-label is required by the TabsList type:
export function AccountTabs(): JSX.Element {
return (
<Tabs defaultTab="overview">
<Tabs.List aria-label="Account sections">
<Tabs.Tab id="overview">Overview</Tabs.Tab>
<Tabs.Tab id="billing">Billing</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id="overview">Account overview content.</Tabs.Panel>
<Tabs.Panel id="billing">Billing details content.</Tabs.Panel>
</Tabs>
);
}
Custom Hooks
Extract reusable logic into a hook when the same stateful behavior is needed in more than one place, or when a component's logic is complex enough to test independently. The generic <T> preserves the caller's type end to end, and the runtime guard rejects delay values that would produce silently wrong behavior.
import { useEffect, useState } from 'react';
export function useDebounce<T>(value: T, delay: number = 500): T {
if (!Number.isFinite(delay) || delay < 0) {
throw new RangeError(
`useDebounce: \`delay\` must be a finite, non-negative number, received ${String(delay)}.`
);
}
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
Usage inside a component — only the settled value reaches onSearch, so a 10-character query triggers one search instead of ten:
import { useEffect, useState } from 'react';
import { useDebounce } from '@/hooks/use-debounce';
interface SearchBoxProps {
onSearch: (query: string) => void;
}
export function SearchBox({ onSearch }: SearchBoxProps): JSX.Element {
const [searchTerm, setSearchTerm] = useState<string>('');
const debouncedSearch = useDebounce<string>(searchTerm, 300);
useEffect(() => {
const query = debouncedSearch.trim();
if (query.length > 0) {
onSearch(query);
}
}, [debouncedSearch, onSearch]);
return (
<input
type="search"
value={searchTerm}
React.ChangeEvent<HTMLInputElement>) =>
setSearchTerm(event.target.value)
}
placeholder="Search..."
aria-label="Search"
className="w-full rounded-md border px-3 py-2"
/>
);
}
Pitfalls
- Defaulting context to an empty object instead of
null. An empty-object default lets children render in a broken state silently. Default tonulland throw a precise error in the reader hook so misuse is caught at the call site, not deep in a child component. - Using
anyto silence type errors. Explicit types end to end (noany) is a hard rule. A clear error at the misuse site is far cheaper to debug than corrupted state discovered later. Validate inputs a caller could realistically get wrong and fail loudly with actionable errors. - Importing entire libraries (
import _ from 'lodash'). Barrel/whole-library imports defeat tree-shaking. This often matters more than the dependency choice itself. Uselodash-eswith tree-shaking or native ES methods. - Shipping
momentor fulllodashto production.momentis ~290KB;date-fnsis ~12KB anddayjsis ~2KB.lodashis ~71KB;lodash-eswith tree-shaking ships only what you use. The exception is a codebase already standardized on one — consistency can outweigh a few KB until a planned migration. - Shipping Next.js experimental features unguarded. Experimental APIs can change or break between minor releases. Use them only behind a clear upgrade plan and with a fallback.
- Skipping accessibility to move faster. Semantic HTML and correct ARIA are part of "done," not an enhancement. Broken keyboard navigation or missing labels exclude real users and are treated as bugs.
- Treating a failing or skipped test as a follow-up. A failing or skipped test is an unfinished change. Fix it before merging.
- Adding all optional features at scaffold time. Each feature pulls in real dependencies. Add features on demand to keep the dependency graph honest.
- Using
python3on Windows PowerShell. On Windows, the command ispython, notpython3. Verify withpython --versionbefore running scripts.
Verification
Run through these checks because each catches a different class of failure the others miss — types confirm the code compiles, the dev server confirms it renders, and accessibility/test passes confirm it works for real users and stays working.
Review the generated project structure and configuration files so the baseline matches your team's conventions before you build on it.
Run the development server and confirm the application renders and is interactive:
npm run devA clean type-check does not prove the UI works — verify in the browser.
Run the bundle analyzer, apply the highest-value fixes (largest size saved for least migration effort), and re-check the score:
python scripts/bundle_analyzer.py . --verboseExercise the feature across the golden path and edge cases (empty, loading, and error states) to catch regressions early.
Verify accessibility: keyboard-only navigation works, interactive elements are labeled, and ARIA roles match the widget pattern.
Run the test suite and confirm it passes:
npm testA failing or skipped test is an unfinished change, not a follow-up.
Review the diff for security and performance issues (unsanitized input, secrets in client code, unnecessary re-renders or heavy imports).
Related Skills
emil-design-eng— animation/motion craft (easing, transform, popover/drawer). Hand off polish-of-feel work; this chair owns scaffold, generators, and bundle analysis.web-interface-guidelines— Vercel UI a11y, CLS, reduced-motion, and Lighthouse execute path. Hand off guideline audits.shadcn— shadcn/ui CLI and component install, not this scaffolder.