Senior Frontend Engineer
Overview
Deliver production-grade frontend code following a structured three-phase workflow: context discovery, development, and handoff. This skill enforces strict quality standards including atomic design component architecture, comprehensive state management patterns, SSR/SSG/ISR optimization, and mandatory >85% test coverage with Vitest, React Testing Library, and Playwright.
Announce at start: "I'm using the senior-frontend skill for production-grade React/TypeScript development."
Phase 1: Context Discovery
Goal: Understand the existing codebase before writing any code.
Actions
- Analyze existing codebase structure and conventions
- Identify the tech stack version (React 18/19, Next.js 14/15, TypeScript version)
- Review existing component library and design system
- Check state management approach already in use
- Understand build tooling and CI pipeline
- Map existing test infrastructure and coverage
STOP — Do NOT proceed to Phase 2 until:
Phase 2: Development
Goal: Implement with strict TypeScript, atomic design, and TDD.
Actions
- Design component architecture following atomic design
- Implement with TypeScript strict mode
- Write tests alongside implementation (TDD when appropriate)
- Optimize for performance (bundle size, rendering, loading)
- Ensure accessibility compliance
Component Architecture Decision Table (Atomic Design)
| Level |
Description |
Business Logic |
Example |
| Atoms |
Smallest building blocks |
None |
Button, Input, Icon, Badge |
| Molecules |
Composed of atoms |
Minimal |
FormField, SearchBar, Card |
| Organisms |
Complex with business logic |
Yes |
DataTable, NavigationBar, CommentThread |
| Templates |
Page structure without data |
Layout only |
DashboardLayout, AuthLayout |
| Pages |
Templates connected to data |
Data fetching |
UsersPage, SettingsPage |
Atom Example
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
}
export function Button({ variant = 'primary', size = 'md', isLoading, children, ...props }: ButtonProps) {
return (
<button className={cn(buttonVariants({ variant, size }))} disabled={isLoading || props.disabled} {...props}>
{isLoading ? <Spinner size={size} /> : children}
</button>
);
}
State Management Decision Table
| State Type |
Solution |
When to Use |
| Server state |
React Query / TanStack Query |
API data, caching, sync |
| Form state |
React Hook Form + Zod |
Form validation, submission |
| Global UI state |
Zustand |
Theme, sidebar open, modals |
| Local UI state |
useState / useReducer |
Component-specific state |
| URL state |
nuqs / useSearchParams |
Filters, pagination, tabs |
| Complex local |
useReducer |
Multiple related state transitions |
| Shared context |
React Context |
Theme, locale, auth (infrequent updates) |
SSR / SSG / ISR Decision Table (Next.js App Router)
| Pattern |
Use When |
Cache Strategy |
| Static (SSG) |
Content rarely changes |
Build time |
| ISR |
Content changes periodically |
Revalidate interval |
| SSR |
Content changes per request |
No cache |
| Client |
User-specific, interactive |
Browser |
Server vs Client Component Decision
| Need |
Component Type |
| Direct data fetching |
Server (default) |
| Event handlers (onClick, onChange) |
Client ('use client') |
| useState / useReducer |
Client |
| useEffect / useLayoutEffect |
Client |
| Browser APIs (window, localStorage) |
Client |
| Third-party libs using client features |
Client |
| No interactivity needed |
Server (default) |
STOP — Do NOT proceed to Phase 3 until:
Phase 3: Handoff
Goal: Verify quality gates and prepare for review.
Actions
- Verify test coverage meets >85% threshold
- Run full lint and type check
- Document complex components with JSDoc/TSDoc
- Create Storybook stories for UI components
- Performance audit (Lighthouse, bundle analysis)
Performance Checklist
Coverage Thresholds
{
"coverageThreshold": {
"global": {
"branches": 85,
"functions": 85,
"lines": 85,
"statements": 85
}
}
}
STOP — Handoff complete when:
Testing Requirements
Unit Tests (Vitest + React Testing Library)
describe('Button', () => {
it('renders children', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
it('shows loading state', () => {
render(<Button isLoading>Click me</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
it('calls onClick when clicked', async () => {
const
render(<Button me</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledOnce();
});
});
Integration Tests
- Component compositions (form submission flow)
- Data fetching with MSW (Mock Service Worker)
- Routing and navigation
- Error boundaries and fallbacks
E2E Tests (Playwright)
test('user can complete checkout', async ({ page }) => {
await page.goto('/products');
await page.getByRole('button', { name: 'Add to cart' }).first().click();
await page.getByRole('link', { name: 'Cart' }).click();
await expect(page.getByText('1 item')).toBeVisible();
await page.getByRole('button', { name: 'Checkout' }).click();
});
React Query Patterns
function useUsers(filters: UserFilters) {
return useQuery({
queryKey: ['users', filters],
queryFn: () => fetchUsers(filters),
staleTime: 5 * 60 * 1000,
placeholderData: keepPreviousData,
});
}
function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateUser,
onMutate: async (newUser) => {
await queryClient.cancelQueries({ queryKey: ['users'] });
const previous = queryClient.getQueryData(['users']);
queryClient.setQueryData(['users'], (old) =>
old.map(u => u.id === newUser.id ? { ...u, ...newUser } : u)
);
return { previous };
},
onError: (err, newUser, context) => {
queryClient.setQueryData(['users'], context.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
Memoization Decision Table
| Technique |
Use When |
Do NOT Use When |
useMemo |
Expensive computation, referential equality for deps |
Simple calculations, primitive values |
useCallback |
Functions passed to memoized children |
Functions not passed as props |
React.memo |
Component re-renders often with same props |
Props change on every render |
| None |
Default — do not memoize |
Always profile first |
Anti-Patterns / Common Mistakes
| Anti-Pattern |
Why It Is Wrong |
Correct Approach |
useEffect for data fetching |
Race conditions, no caching, no dedup |
React Query or Server Components |
| Prop drilling more than 2 levels |
Tight coupling, maintenance burden |
Composition, context, or Zustand |
| Business logic in components |
Untestable, unreusable |
Extract to hooks or utility functions |
| Barrel exports |
Breaks tree-shaking, slower builds |
Direct imports |
| Testing implementation details |
Brittle tests that break on refactor |
Test behavior: user actions and outcomes |
any type anywhere |
Defeats TypeScript's purpose |
unknown + type guards |
| Inline styles for non-dynamic values |
Inconsistent, hard to maintain |
CSS modules, Tailwind, or styled-components |
| Memoizing everything |
Adds complexity, often slower |
Profile first, memoize second |
Documentation Lookup (Context7)
Use mcp__context7__resolve-library-id then mcp__context7__query-docs for up-to-date docs. Returned docs override memorized knowledge.
react — when uncertain about hooks API, component lifecycle, or React 19+ features
next.js — for App Router, Server Components, or Next.js-specific APIs
typescript — for advanced type patterns or compiler options
tailwindcss — for utility classes, configuration, or plugin API
vitest — for test runner API, matchers, or mock utilities
Integration Points
| Skill |
Relationship |
testing-strategy |
Strategy defines frontend test frameworks |
test-driven-development |
Components are built with TDD cycle |
react-best-practices |
Detailed React patterns complement this skill |
performance-optimization |
Frontend performance follows optimization methodology |
code-review |
Review verifies component architecture and test coverage |
clean-code |
Code quality principles apply to component code |
webapp-testing |
Playwright E2E tests use this skill's page structure |
acceptance-testing |
UI acceptance criteria drive component tests |
Key Principles
- TypeScript strict mode, no
any (use unknown + type guards)
- Prefer composition over inheritance
- Colocate tests, styles, and stories with components
- Server Components by default; Client Components only when required
- Error boundaries at route and feature boundaries
- Accessibility is not optional (test with axe-core)
Skill Type
FLEXIBLE — Adapt component architecture and state management to the existing project conventions. The three-phase workflow is strongly recommended. Test coverage must target >85%. TypeScript strict mode is non-negotiable.
1---2name: senior-frontend3description: Use when the user needs production-grade React/Next.js/TypeScript development with rigorous component architecture, state management, performance optimization, and >85% test coverage. Triggers: React component development, Next.js page creation, state management design, frontend performance audit, component library setup.4---5
6# Senior Frontend Engineer
7
8## Overview
9
10Deliver production-grade frontend code following a structured three-phase workflow: context discovery, development, and handoff. This skill enforces strict quality standards including atomic design component architecture, comprehensive state management patterns, SSR/SSG/ISR optimization, and mandatory >85% test coverage with Vitest, React Testing Library, and Playwright.
11
12**Announce at start:** "I'm using the senior-frontend skill for production-grade React/TypeScript development."
13
14---
15
16## Phase 1: Context Discovery
17
18**Goal:** Understand the existing codebase before writing any code.
19
20### Actions
21
221. Analyze existing codebase structure and conventions
232. Identify the tech stack version (React 18/19, Next.js 14/15, TypeScript version)
243. Review existing component library and design system
254. Check state management approach already in use
265. Understand build tooling and CI pipeline
276. Map existing test infrastructure and coverage
28
29### STOP — Do NOT proceed to Phase 2 until:
30- [ ] Tech stack versions are identified
31- [ ] Existing patterns and conventions are documented
32- [ ] Test infrastructure is mapped
33- [ ] State management approach is identified
34
35---
36
37## Phase 2: Development
38
39**Goal:** Implement with strict TypeScript, atomic design, and TDD.
40
41### Actions
42
431. Design component architecture following atomic design
442. Implement with TypeScript strict mode
453. Write tests alongside implementation (TDD when appropriate)
464. Optimize for performance (bundle size, rendering, loading)
475. Ensure accessibility compliance
48
49### Component Architecture Decision Table (Atomic Design)
50
51| Level | Description | Business Logic | Example |
52|-------|------------|---------------|---------|
53| **Atoms** | Smallest building blocks | None | Button, Input, Icon, Badge |
54| **Molecules** | Composed of atoms | Minimal | FormField, SearchBar, Card |
55| **Organisms** | Complex with business logic | Yes | DataTable, NavigationBar, CommentThread |
56| **Templates** | Page structure without data | Layout only | DashboardLayout, AuthLayout |
57| **Pages** | Templates connected to data | Data fetching | UsersPage, SettingsPage |
58
59### Atom Example
60
61```typescript
62interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
63 variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
64 size?: 'sm' | 'md' | 'lg';
65 isLoading?: boolean;
66}
67
68export function Button({ variant = 'primary', size = 'md', isLoading, children, ...props }: ButtonProps) {
69 return (
70 <button className={cn(buttonVariants({ variant, size }))} disabled={isLoading || props.disabled} {...props}>
71 {isLoading ? <Spinner size={size} /> : children}
72 </button>
73 );
74}
75```
76
77### State Management Decision Table
78
79| State Type | Solution | When to Use |
80|------------|----------|-------------|
81| Server state | React Query / TanStack Query | API data, caching, sync |
82| Form state | React Hook Form + Zod | Form validation, submission |
83| Global UI state | Zustand | Theme, sidebar open, modals |
84| Local UI state | useState / useReducer | Component-specific state |
85| URL state | nuqs / useSearchParams | Filters, pagination, tabs |
86| Complex local | useReducer | Multiple related state transitions |
87| Shared context | React Context | Theme, locale, auth (infrequent updates) |
88
89### SSR / SSG / ISR Decision Table (Next.js App Router)
90
91| Pattern | Use When | Cache Strategy |
92|---------|----------|---------------|
93| Static (SSG) | Content rarely changes | Build time |
94| ISR | Content changes periodically | Revalidate interval |
95| SSR | Content changes per request | No cache |
96| Client | User-specific, interactive | Browser |
97
98### Server vs Client Component Decision
99
100| Need | Component Type |
101|------|---------------|
102| Direct data fetching | Server (default) |
103| Event handlers (onClick, onChange) | Client (`'use client'`) |
104| useState / useReducer | Client |
105| useEffect / useLayoutEffect | Client |
106| Browser APIs (window, localStorage) | Client |
107| Third-party libs using client features | Client |
108| No interactivity needed | Server (default) |
109
110### STOP — Do NOT proceed to Phase 3 until:
111- [ ] Components follow atomic design hierarchy
112- [ ] TypeScript strict mode is enabled, no `any` types
113- [ ] Tests are written for all components
114- [ ] Accessibility is verified (axe-core)
115
116---
117
118## Phase 3: Handoff
119
120**Goal:** Verify quality gates and prepare for review.
121
122### Actions
123
1241. Verify test coverage meets >85% threshold
1252. Run full lint and type check
1263. Document complex components with JSDoc/TSDoc
1274. Create Storybook stories for UI components
1285. Performance audit (Lighthouse, bundle analysis)
129
130### Performance Checklist
131
132- [ ] Bundle size < 200KB gzipped (initial load)
133- [ ] Largest Contentful Paint < 2.5s
134- [ ] First Input Delay < 100ms
135- [ ] Cumulative Layout Shift < 0.1
136- [ ] Images: next/image with proper sizing and formats
137- [ ] Fonts: next/font with display swap
138- [ ] No layout thrashing (batch DOM reads/writes)
139- [ ] Virtualization for lists > 100 items
140
141### Coverage Thresholds
142
143```json
144{
145 "coverageThreshold": {
146 "global": {
147 "branches": 85,
148 "functions": 85,
149 "lines": 85,
150 "statements": 85
151 }
152 }
153}
154```
155
156### STOP — Handoff complete when:
157- [ ] Test coverage >85% verified
158- [ ] Lint and type check pass with zero errors
159- [ ] Performance audit completed
160- [ ] Complex components documented
161
162---
163
164## Testing Requirements
165
166### Unit Tests (Vitest + React Testing Library)
167
168```typescript
169describe('Button', () => {
170 it('renders children', () => {
171 render(<Button>Click me</Button>);
172 expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
173 });
174
175 it('shows loading state', () => {
176 render(<Button isLoading>Click me</Button>);
177 expect(screen.getByRole('button')).toBeDisabled();
178 });
179
180 it('calls onClick when clicked', async () => {
181 const onClick = vi.fn();
182 render(<Button onClick={onClick}>Click me</Button>);
183 await userEvent.click(screen.getByRole('button'));
184 expect(onClick).toHaveBeenCalledOnce();
185 });
186});
187```
188
189### Integration Tests
190
191- Component compositions (form submission flow)
192- Data fetching with MSW (Mock Service Worker)
193- Routing and navigation
194- Error boundaries and fallbacks
195
196### E2E Tests (Playwright)
197
198```typescript
199test('user can complete checkout', async ({ page }) => {
200 await page.goto('/products');
201 await page.getByRole('button', { name: 'Add to cart' }).first().click();
202 await page.getByRole('link', { name: 'Cart' }).click();
203 await expect(page.getByText('1 item')).toBeVisible();
204 await page.getByRole('button', { name: 'Checkout' }).click();
205});
206```
207
208---
209
210## React Query Patterns
211
212```typescript
213function useUsers(filters: UserFilters) {
214 return useQuery({
215 queryKey: ['users', filters],
216 queryFn: () => fetchUsers(filters),
217 staleTime: 5 * 60 * 1000,
218 placeholderData: keepPreviousData,
219 });
220}
221
222function useUpdateUser() {
223 const queryClient = useQueryClient();
224 return useMutation({
225 mutationFn: updateUser,
226 onMutate: async (newUser) => {
227 await queryClient.cancelQueries({ queryKey: ['users'] });
228 const previous = queryClient.getQueryData(['users']);
229 queryClient.setQueryData(['users'], (old) =>
230 old.map(u => u.id === newUser.id ? { ...u, ...newUser } : u)
231 );
232 return { previous };
233 },
234 onError: (err, newUser, context) => {
235 queryClient.setQueryData(['users'], context.previous);
236 },
237 onSettled: () => {
238 queryClient.invalidateQueries({ queryKey: ['users'] });
239 },
240 });
241}
242```
243
244---
245
246## Memoization Decision Table
247
248| Technique | Use When | Do NOT Use When |
249|-----------|----------|----------------|
250| `useMemo` | Expensive computation, referential equality for deps | Simple calculations, primitive values |
251| `useCallback` | Functions passed to memoized children | Functions not passed as props |
252| `React.memo` | Component re-renders often with same props | Props change on every render |
253| None | Default — do not memoize | Always profile first |
254
255---
256
257## Anti-Patterns / Common Mistakes
258
259| Anti-Pattern | Why It Is Wrong | Correct Approach |
260|-------------|----------------|-----------------|
261| `useEffect` for data fetching | Race conditions, no caching, no dedup | React Query or Server Components |
262| Prop drilling more than 2 levels | Tight coupling, maintenance burden | Composition, context, or Zustand |
263| Business logic in components | Untestable, unreusable | Extract to hooks or utility functions |
264| Barrel exports | Breaks tree-shaking, slower builds | Direct imports |
265| Testing implementation details | Brittle tests that break on refactor | Test behavior: user actions and outcomes |
266| `any` type anywhere | Defeats TypeScript's purpose | `unknown` + type guards |
267| Inline styles for non-dynamic values | Inconsistent, hard to maintain | CSS modules, Tailwind, or styled-components |
268| Memoizing everything | Adds complexity, often slower | Profile first, memoize second |
269
270---
271
272## Documentation Lookup (Context7)
273
274Use `mcp__context7__resolve-library-id` then `mcp__context7__query-docs` for up-to-date docs. Returned docs override memorized knowledge.
275- `react` — when uncertain about hooks API, component lifecycle, or React 19+ features
276- `next.js` — for App Router, Server Components, or Next.js-specific APIs
277- `typescript` — for advanced type patterns or compiler options
278- `tailwindcss` — for utility classes, configuration, or plugin API
279- `vitest` — for test runner API, matchers, or mock utilities
280
281---
282
283## Integration Points
284
285| Skill | Relationship |
286|-------|-------------|
287| `testing-strategy` | Strategy defines frontend test frameworks |
288| `test-driven-development` | Components are built with TDD cycle |
289| `react-best-practices` | Detailed React patterns complement this skill |
290| `performance-optimization` | Frontend performance follows optimization methodology |
291| `code-review` | Review verifies component architecture and test coverage |
292| `clean-code` | Code quality principles apply to component code |
293| `webapp-testing` | Playwright E2E tests use this skill's page structure |
294| `acceptance-testing` | UI acceptance criteria drive component tests |
295
296---
297
298## Key Principles
299
300- TypeScript strict mode, no `any` (use `unknown` + type guards)
301- Prefer composition over inheritance
302- Colocate tests, styles, and stories with components
303- Server Components by default; Client Components only when required
304- Error boundaries at route and feature boundaries
305- Accessibility is not optional (test with axe-core)
306
307---
308
309## Skill Type
310
311**FLEXIBLE** — Adapt component architecture and state management to the existing project conventions. The three-phase workflow is strongly recommended. Test coverage must target >85%. TypeScript strict mode is non-negotiable.