---
name: react-frontend-engineering
description: "Build, review, refactor, and test React + TypeScript frontend code using Suraj's SPA-first frontend conventions. Use when working on React components, hooks, feature architecture, routing, state management, React Query data flows, Zustand stores, performance, observability, accessibility, or frontend tests."
---

# React Frontend Engineering Skill

Use this skill whenever the task involves React, TypeScript frontend code, SPA architecture, component design, hooks, frontend testing, performance, observability, or frontend refactoring.

The goal is to produce production-grade React code that is simple, typed, maintainable, testable, and aligned with Suraj's preferred frontend style.

## Choosing the architecture

When the user has not specified a framework, pick the architecture from the use case before writing any code:

- **Astro** is the default for static sites, blogs, marketing pages, docs, and anything where SEO or first-load performance is critical. Ship static HTML and add interactivity through islands; reach for React only inside the islands that need it.
- **React SPA** is the default for auth-gated clients, dashboards, and app-like surfaces where SEO is not a concern. Use TanStack Query as the data layer.

If the user names a framework, or the existing repository has already chosen one, follow that instead.

## Default stack

Assume this stack unless the repository clearly uses something else:

- Astro for static / SEO-critical sites (React used inside islands)
- React + TypeScript for SPAs
- Vite
- TanStack Router for routing
- TanStack Query for server state
- Zustand for small-to-medium global client state
- XState for complex state machines (editors, multi-step flows, anything with rich states/transitions)
- Tailwind CSS for styling
- pnpm for package management
- Vitest for unit tests
- React Testing Library for component behavior
- Playwright for end-to-end tests
- Sentry for frontend error monitoring

If the existing codebase uses a different library, follow the codebase instead of forcing this stack.

## Core engineering principles

- **Prefer composition above all else.** Build behaviour by composing small pieces (components, hooks, and helpers) rather than adding options to one large configurable component or reaching for inheritance.
- **Check for an existing implementation before writing new code.** Search the codebase for a component, hook, util, type, or query that already does the job; reuse or extend it instead of writing a parallel version.
- **Stay DRY: keep a single source of truth.** Never duplicate the same logic, type, constant, or query in two places. Extract it once and import it.
- Prefer feature-oriented architecture.
- Keep shared UI separate from feature-specific UI.
- Separate UI, business logic, data fetching, and state.
- Keep components small and responsibility-focused.
- Use TypeScript explicitly for public APIs, props, shared types, API responses, and state contracts.
- Avoid `any`; use `unknown`, discriminated unions, generics, or narrower types instead.
- Prefer readable code over clever code.
- Prefer early returns over deep nesting.
- Keep local UI state local.
- Keep server state in TanStack Query.
- Do not duplicate server state into client stores.

## File and folder conventions

Prefer kebab-case file and folder names.

Recommended feature layout:

```txt
src/
  components/
    ui/                  # shared reusable primitives, shadcn-style
  features/
    feature-name/
      components/        # feature-specific components
      hooks/             # feature hooks
      api/               # feature API calls/query options
      stores/            # feature-local Zustand stores only if needed
      types/             # feature-specific types
      utils/             # feature-specific pure helpers
      index.ts
  routes/
  lib/
  hooks/
  types/
```

Naming conventions:

- Component files: `user-card.tsx`
- Hook files: `use-user-profile.ts`
- Utility files: `format-price.ts`
- Test files: `user-card.test.tsx`, `format-price.test.ts`
- E2E tests: `user-profile.spec.ts`

## Component rules

When writing components:

- Define a `Props` type near the component unless the props are shared.
- Keep props minimal and explicit.
- Avoid passing large objects when only a few fields are needed.
- Prefer controlled components when the parent owns the state.
- Prefer uncontrolled/local state for isolated UI state.
- Keep rendering declarative.
- Extract complex conditional rendering into small functions or subcomponents.
- Avoid side effects during render.
- Do not prematurely use `memo`, `useMemo`, or `useCallback`.
- Use memoization only when there is a real render or computation problem.

Preferred component shape:

```tsx
type Props = {
  title: string;
  description?: string;
  onSelect: () => void;
};

export function ExampleCard({ title, description, onSelect }: Props) {
  return (
    <button type="button" onClick={onSelect} className="rounded-lg border p-4 text-left">
      <h3 className="font-medium">{title}</h3>
      {description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
    </button>
  );
}
```

## Hooks

Use hooks to isolate reusable stateful behavior, data orchestration, subscriptions, browser APIs, and feature workflows.

Hook rules:

- Name hook files `use-thing.ts`.
- Keep hooks focused; one hook should not become a feature service layer.
- Do not hide UI side effects in generic hooks unless the name makes that behavior clear.
- Return stable, minimal values.
- Prefer object returns when returning multiple values.
- Keep pure transformations outside hooks where possible.

## Data fetching and server state

Use TanStack Query for all server state.

Rules:

- Put API calls and query options close to the feature.
- Use typed request and response contracts.
- Use stable query keys.
- Prefer query option factories for reuse.
- Handle loading, error, empty, and success states deliberately.
- Do not copy query data into component state unless the user is editing a draft form.
- Use mutations for writes and invalidate/update relevant queries intentionally.
- Centralise query keys in a key factory so they stay consistent and refactor-safe.
- Tune `staleTime`/`gcTime` per query instead of leaving everything default; set shared defaults once on the `QueryClient`.
- Use `enabled` for dependent queries and prefetch/parallel queries to avoid request waterfalls.
- Use `select` to subscribe to a narrow, derived slice and avoid needless re-renders.
- For optimistic updates, snapshot the previous value and roll back in `onError`.
- Invalidate the smallest set of keys that actually changed; avoid blanket invalidation.

Example pattern:

```ts
import { queryOptions } from '@tanstack/react-query';

export type User = {
  id: string;
  name: string;
};

export async function getUser(userId: string): Promise<User> {
  const response = await fetch(`/api/users/${userId}`);

  if (!response.ok) {
    throw new Error('Failed to load user');
  }

  return response.json() as Promise<User>;
}

export function userQueryOptions(userId: string) {
  return queryOptions({
    queryKey: ['user', userId],
    queryFn: () => getUser(userId),
  });
}
```

## Client state management

Escalate deliberately. Reach for the simplest tool that fits and only move up when it stops being suitable:

1. **Local state** (`useState`/`useReducer`) for state owned by one component or its small subtree.
2. **React Context** for state shared across a subtree that changes infrequently (theme, current user, locale). Do not use Context for high-frequency updates; it re-renders every consumer.
3. **Zustand** at small-to-medium complexity when Context is no longer suitable (frequent updates, cross-cutting client state, or selective subscriptions).
4. **XState** when the state is genuinely complex: editors, multi-step wizards, drag-and-drop, anything with many states, transitions, guards, or side effects that benefit from an explicit state machine.

Whatever the tool, this is **client** state only. Never store fetched/server state here; that belongs in TanStack Query.

Good client-state candidates: UI shell state, user preferences, command palette, modal/sidebar state, workflow state that spans routes.

Not client state: fetched API data, cached server state, single-screen form state, or anything derivable from URL/search params.

Keep stores and machines small, typed, and subscribed to via narrow selectors.

## Routing and URL state

Use TanStack Router for SPA routes.

Rules:

- Prefer URL search params for shareable filters, tabs, pagination, and sort state.
- Keep route loaders/query prefetching predictable.
- Avoid hidden global state for route-specific state.
- Keep route modules thin; move feature code into `features/*`.

## Forms

- Keep form state local to the form unless the workflow spans routes.
- Validate inputs close to submission and near fields where useful.
- Use typed schemas if the project already uses a schema library.
- Keep API DTOs separate from UI form state when their shapes differ.
- Show clear validation and submission errors.

## Comments and documentation

Let the code and the types carry the meaning; comment only where they cannot.

- Do not over-comment. If a comment just restates what the code or the TypeScript type already says, delete it.
- Add a comment only where it genuinely helps a human or an agent: the non-obvious "why", a tradeoff, a workaround, an invariant, or a gotcha.
- Use JSDoc for the important, shared surface: exported functions and hooks, public component props, complex or ambiguous types, and anything reused across features. A good JSDoc summary plus `@param`/`@returns` where the name isn't self-explanatory is enough.
- Keep comments and JSDoc accurate when you change the code; a stale comment is worse than none.

## Performance rules

Prioritize simple code first, then optimize when needed.

Default performance checklist:

- Lazy-load routes and heavy feature modules.
- Avoid expensive computation during render.
- Use virtualization for large lists.
- Avoid unnecessary global state subscriptions.
- Select narrow slices from Zustand stores.
- Keep bundle size in mind before adding dependencies.
- Use `React.memo`, `useMemo`, and `useCallback` only when they solve a measured or obvious problem.
- Avoid re-rendering large trees from high-frequency state updates.

## Observability and resilience

Frontend code should be debuggable in production.

Add or preserve:

- Error boundaries around route-level or feature-level failure zones.
- Sentry capture for unexpected errors where appropriate.
- Meaningful error messages for users.
- Empty states when a list has no data.
- Loading states that avoid layout jumps where practical.
- Basic breadcrumbs/context for high-value workflows if Sentry is configured.

Never swallow errors silently.

## Accessibility

Accessibility is required, not optional.

Checklist:

- Use semantic HTML first.
- Buttons must be buttons, links must be links.
- Add accessible labels for icon-only buttons.
- Preserve keyboard navigation.
- Maintain visible focus states.
- Use ARIA only when semantic HTML is not enough.
- Ensure form inputs have labels and errors are announced or associated.
- Do not rely on color alone to communicate state.

## Testing strategy

Test behavior, not implementation details.

Use:

- Vitest for pure utilities and hooks where appropriate.
- React Testing Library for component behavior.
- Playwright for critical user journeys.

Prioritize tests for:

- Critical product flows
- Data-state transitions
- Permission/auth boundaries
- Complex conditional UI
- Regression-prone utilities

Avoid low-value snapshot tests and brittle tests that assert implementation details.

## Refactoring workflow

When asked to refactor existing React code:

1. Preserve behavior first.
2. Identify state ownership: local, URL, server, or global client state.
3. Move server state to TanStack Query if it is incorrectly stored elsewhere.
4. Split large components by responsibility.
5. Extract pure helpers before extracting hooks.
6. Keep public props/API stable unless the user explicitly wants a breaking change.
7. Add or update tests for critical behavior when practical.

## Review checklist

Before finalizing React code, check:

- Types are explicit and no `any` was introduced.
- No duplication was introduced; existing implementations were reused where they exist.
- Behaviour is composed from small pieces rather than configuration flags or inheritance.
- Server state is handled by TanStack Query, following its best practices (stable keys, scoped invalidation, deliberate state handling).
- Client state uses the right tier (local → context → Zustand → XState) and is not overused.
- Components are small and readable.
- Loading, error, empty, and success states are handled.
- Accessibility basics are covered.
- No unnecessary memoization was added.
- No obvious render-time expensive work exists.
- Comments are minimal and meaningful; JSDoc covers the important shared surface.
- Code follows the existing repository style.

## Response style when using this skill

When giving code:

- Be direct and practical.
- Prefer complete, paste-ready snippets.
- Mention assumptions only when they affect implementation.
- Explain important tradeoffs briefly.
- Do not over-explain basic React concepts unless asked.
- If touching multiple files, show paths clearly.
