Skip to content

React / Senior Frontend Engineering

FieldValue
TypeSkill Resource
Source~/.copilot/skills/frontend/references/react.md
DescriptionNot specified

Source Content

React / Senior Frontend Engineering

Senior React specialist material: React 19, Server Components, state-location decisions, hooks, testing, and the day-to-day craft of typed, testable, accessible UI. Absorbed from the former senior-frontend skill (plus its bundled frontend_best_practices.md, react_patterns.md, and nextjs_optimization_guide.md references).

Use for

  • New React components and features in a TypeScript codebase.
  • State decisions: local vs URL vs Context vs Zustand vs server cache.
  • React 19 work — use(), useActionState, async transitions, Server Components.
  • Data fetching with TanStack Query and Router loaders.
  • Hook design and effect cleanup; converting class components to hooks/RSC.

Don’t use for

  • Visual direction, hero pages, aesthetic work → references/frontend-design.md.
  • Design tokens, primitives, theming → ui-design-system skill.
  • Performance audits and bundle budgets → references/react-perf.md.
  • Route topology, search params, loaders-as-data-graph → references/tanstack-router.md.

How to work

  1. Analyze. Component hierarchy, where state lives, where data comes from, what re-renders.
  2. Pick patterns. State location (local / URL / Zustand / Query). Data fetching (loader / use() / Query). Server vs Client component.
  3. Implement. TypeScript strict, semantic HTML, keys stable, effects cleaned up, Suspense boundaries for async.
  4. Type-check. tsc --noEmit. Fix every error before continuing. Don’t paper over with any or as.
  5. Test. React Testing Library; integration over unit. Assert behavior the user can observe.
  6. Memoize only with evidence. React.memo / useMemo / useCallback after profiling, not before.
  7. Hand off. Brief decision log: what was picked, what was rejected, why.

Examples

  • “Build a <UserMenu> component with avatar, dropdown, and signout” → write it in TS strict with semantic HTML, accessible roles, focus management, and useActionState for the signout — no any, no untyped event handlers.
  • “Where should the filter/sort/page state live in this table?” → in the URL via TanStack Router search params, not useState.
  • “Convert this class component to hooks” → migrate, name effect cleanup explicitly, and call out any subtle behavior change (mount/unmount semantics, stale closure risk).
  • “Should this be a Server Component or Client Component?” → ask one question (App Router? data needs? interactivity?) then pick — Server by default, Client when interactivity or browser-only APIs force it.

Self-rubric

  • Typed end-to-end. No any, no unjustified as. tsc --noEmit is clean.
  • State lives in the right place. Filter/sort/page in the URL, not useState.
  • No needless memoization. Memo is a perf fix with evidence, not a default.
  • Effects clean up. Every subscription, timer, listener has a return.
  • Accessible by default. Semantic elements, labels, focus order, contrast.
  • Tested at the level that matters. Integration covers the user path; units cover the gnarly logic.
  • Validated: scripts/check_frontend.sh [TARGET_REPO_PATH] exits 0.

Constraints (MUST / MUST NOT)

MUST: TypeScript strict mode · stable keys · effect cleanup · semantic HTML + ARIA · error boundaries in production · Suspense for async. MUST NOT: mutate state directly · use array index as key for dynamic lists · create new functions/objects in JSX passed to memoized children · ignore strict-mode warnings.

Validate

scripts/check_frontend.sh [TARGET_REPO_PATH] — prefers oxlint (50-100x faster than eslint) with the bundled templates/oxlint/.oxlintrc.json config, which adds a custom JS plugin rule (no-unnecessary-react-import) on top of oxlint’s defaults plus typescript/no-explicit-any as a hard error; auto-enables --type-aware when oxlint-tsgolint is installed in the target repo (experimental upstream). Falls back to eslint if oxlint isn’t installed, and to grep-based any/React-import checks if neither is. Always runs tsc --noEmit, hard-fails when tsconfig.json lacks "strict": true, and warns (fuzzy heuristic) on a useEffect that looks like derived state synced via a single setState call.


Deep reference — Accessibility (a11y)

Semantic HTML

// BAD - Divs for everything
<div onClick={handleClick}>Click me</div>
// GOOD - Semantic elements
<button onClick={handleClick}>Click me</button>
<header>...</header>
<nav>...</nav>
<main>...</main>
<article>...</article>
<aside>...</aside>
<footer>...</footer>

Keyboard navigation and focus trapping

function Modal({ isOpen, onClose, children }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen) {
const focusable = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
(focusable?.[0] as HTMLElement)?.focus();
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>
</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>

Screen-reader-only content

const srOnly = 'absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0';
<a href="#main-content" className={srOnly + ' focus:not-sr-only focus:absolute focus:top-0'}>
Skip to main content
</a>
<button aria-label="Close menu">
<XIcon aria-hidden="true" />
</button>
<button>
<XIcon aria-hidden="true" />
<span className={srOnly}>Close menu</span>
</button>

Deep reference — Testing strategies

Component testing with Testing Library

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');
});
});

Hook testing

import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('increments count', () => {
const { result } = renderHook(() => useCounter());
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
});

Integration testing

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');
});
});
});

E2E testing with Playwright

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');
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="address"]', '123 Test St');
await page.click('text=Place Order');
await expect(page).toHaveURL(/\/order\/confirmation/);
});
});

Deep reference — TypeScript patterns

Component props and polymorphic components

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>
);
}
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>;
}

Discriminated unions for state machines

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} />;
}
}

Generic components and type guards

interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={keyExtractor(item)}>{renderItem(item, i)}</li>)}</ul>;
}
interface User { id: string; name: string; }
interface Admin extends User { role: 'admin'; permissions: string[]; }
function isAdmin(user: User): user is Admin {
return 'role' in user && (user as Admin).role === 'admin';
}

Deep reference — Tailwind CSS in React

Component variants with CVA

import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
'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-primary text-primary-foreground hover:bg-surface-strong',
secondary: 'bg-secondary text-secondary-foreground hover:bg-muted',
ghost: 'hover:bg-secondary',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
},
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} />;
}

Note: use semantic tokens (bg-primary, text-destructive-foreground) per ADR-001/ADR-006 — never raw Tailwind palette classes like bg-blue-600. The upstream example this was adapted from used raw palette colors; this stack always substitutes semantic tokens.

Responsive design

<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6 lg:gap-8">
{products.map(product => <ProductCard key={product.id} product={product} />)}
</div>
<nav className="hidden md:flex">Desktop nav</nav>
<button className="md:hidden">Mobile menu</button>

Deep reference — Project structure and barrel exports

src/
├── app/ # or src/routes for TanStack Router
├── components/
│ ├── ui/ # Shared UI components
│ └── features/ # Feature-specific components
├── hooks/ # Custom React hooks
├── lib/ # Utilities and configs
├── types/ # TypeScript types
└── styles/
// components/ui/index.ts — barrel export
export { Button } from './Button';
export { Input } from './Input';
export { Card, CardHeader, CardContent, CardFooter } from './Card';

Deep reference — Security

XSS prevention

React escapes content by default. When rendering HTML content, 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 with Zod (see references/zod-schemas.md for the full pattern)

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'),
});
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} />
<Button type="submit">Register</Button>
</form>
);
}

Secure API calls

Never include secrets in client code — proxy through a server route/endpoint that reads server-side env vars, never NEXT_PUBLIC_*/PUBLIC_*-prefixed secrets.

Deep reference — Composition patterns

Compound components

const SelectContext = createContext<SelectContextType | null>(null);
function Select({ children, value, onChange }: SelectProps) {
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>;
}
Select.Trigger = SelectTrigger;

Render props and HOCs

function MouseTracker({ render }: { render: (pos: { x: number; y: number }) => React.ReactNode }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e: MouseEvent) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
return <>{render(position)}</>;
}
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} />;
};
}

Deep reference — Custom hooks

// useDebounce
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;
}
// useLocalStorage
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)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== 'undefined') window.localStorage.setItem(key, JSON.stringify(valueToStore));
}, [key, storedValue]);
return [storedValue, setValue] as const;
}
// useMediaQuery
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;
}

Deep reference — Zustand state management

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' }
)
);

Deep reference — Performance patterns (see references/react-perf.md for the full audit workflow)

// useMemo for expensive calculations
const processedData = useMemo(() => {
let result = data.filter(item => item.name.toLowerCase().includes(filterText.toLowerCase()));
return [...result].sort((a, b) => (a[sortColumn] < b[sortColumn] ? -1 : 1));
}, [data, sortColumn, filterText]);
// 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,
overscan: 5,
});
return (
<div ref={parentRef} className="h-[400px] overflow-auto">
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map(row => (
<div key={row.key} style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: `${row.size}px`, transform: `translateY(${row.start}px)` }}>
{items[row.index].name}
</div>
))}
</div>
</div>
);
}

Deep reference — Error boundaries

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);
}
render() {
if (this.state.hasError) {
return this.props.fallback || <ErrorFallback error={this.state.error} />;
}
return this.props.children;
}
}

Combine with Suspense: <ErrorBoundary fallback={<ErrorMessage />}><Suspense fallback={<Spinner />}><AsyncDataLoader /></Suspense></ErrorBoundary>.

Deep reference — Anti-patterns to refuse

// BAD - Creates new object every render
<Component style={{ color: 'red' }} items={[1, 2, 3]} />
// GOOD
const style = { color: 'red' };
<Component style={style} items={items} />
// BAD - Index keys break with reordering/filtering
{items.map((item, index) => <Item key={index} data={item} />)}
// GOOD
{items.map(item => <Item key={item.id} data={item} />)}
// BAD - Prop drilling through many levels
// GOOD - Context or Zustand for cross-cutting state
// BAD - Mutating state directly
items.push(item); setItems(items);
// GOOD
setItems(prev => [...prev, item]);
// BAD - useEffect for derived state
useEffect(() => { setTotal(items.reduce((s, i) => s + i.price, 0)); }, [items]);
// GOOD - compute during render, or useMemo for expensive calcs
const total = items.reduce((s, i) => s + i.price, 0);

Deep reference — Rendering strategies for SSR/RSC frameworks (Next.js-derived, applies to any RSC-capable meta-framework)

Server Components render on the server and send HTML to the client — default for data-heavy, non-interactive content. Use 'use client' only when a component needs event handlers, state, effects, or browser APIs.

// Server Component (default) — runs on the server, no client bundle impact
async function ProductsPage() {
const products = await db.products.findMany();
return <div className="grid grid-cols-3 gap-4">{products.map(p => <ProductCard key={p.id} product={p} />)}</div>;
}
// Client Component — only where interactivity is required
'use client';
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>;
}

Streaming with Suspense

async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={params.id} />
</Suspense>
</div>
);
}

Parallel data fetching

async function Dashboard() {
const [user, stats, notifications] = await Promise.all([getUser(), getStats(), getNotifications()]);
return <div><UserHeader user={user} /><StatsPanel stats={stats} /><NotificationList notifications={notifications} /></div>;
}

Image optimization checklist (framework-agnostic)

  • Explicit width/height or aspect-ratio to prevent CLS.
  • loading="lazy" below the fold; eager/priority only for the LCP image.
  • AVIF/WebP with a fallback; responsive sizes for art-directed breakpoints.
  • Skeleton placeholders while dimensions are known but bytes aren’t loaded.

Bundle optimization checklist

  • Tree-shake imports: import debounce from 'lodash/debounce', not the whole library.
  • Dynamic import() / React.lazy for routes and heavy widgets off the critical path.
  • Analyze with rollup-plugin-visualizer or the framework’s bundle analyzer; budget every route.

Core Web Vitals quick reference

AreaOptimizationImpact
ImagesPriority/eager for the LCP image, lazy elsewhereHigh
FontsSystem font stack only (ADR-003) — no web font load costN/A (already zero)
CodeDynamic imports for heavy/below-fold componentsHigh
DataParallel fetching with Promise.allHigh
RenderServer Components / static generation by defaultHigh
CacheExplicit revalidate/staleTime — never accidental always-freshMedium

References