React + TypeScript AI Rules

Rules for plain React + TypeScript projects (Vite, no framework). Functional components only, hooks conventions, named exports, strict TS, and a reasonable default for state and data fetching.

TypeScriptReact#react#typescript#vite#functional-componentsLast updated 2026-05-05
tune

Want to customize this rules file? Open the generator with this stack pre-loaded.

Open in generatorarrow_forward

Save at .cursor/rules/main.mdc

React + TypeScript (Vite)

Project context

This is a React 19 + TypeScript single-page app built with Vite. No framework — routing, data fetching, and state are explicit. Functional components only, hooks-first, named exports. The codebase favors explicit composition over deep abstraction.

Stack

  • React 19
  • TypeScript 5+ in strict mode ("strict": true, "noUncheckedIndexedAccess": true)
  • Vite 5+ as the build tool
  • Tailwind CSS 4 for styling
  • React Router v7 (react-router-dom) or TanStack Router for routing
  • TanStack Query for server state (when needed)
  • Vitest + Testing Library for tests
  • pnpm

Folder structure

src/
  main.tsx                — Vite entrypoint
  App.tsx                 — root component
  routes/                 — route components, one file per page
  components/             — reusable components, organized by feature
  hooks/                  — reusable hooks (only when shared)
  lib/                    — utilities and integrations
  api/                    — API client functions, one file per resource
  types/                  — shared types
  styles/                 — global styles

Components

  • Functional only. No class components. No React.FC — type props with an inline interface.
  • Named exports only. No default exports. Makes find-references and rename refactors reliable.
  • One component per file unless a tightly-coupled child component is only used inside the same file.
  • Props interfaces inline:
interface ButtonProps {
  children: React.ReactNode
  onClick: () => void
  variant?: 'primary' | 'ghost'
}

export function Button({ children, onClick, variant = 'primary' }: ButtonProps) {
  return <button onClick={onClick} className={...}>{children}</button>
}
  • Children typed as React.ReactNode, not JSX.Element
  • Event handlers typed via React.ChangeEvent<HTMLInputElement>, not any

Hooks

  • Custom hooks live in src/hooks/ and start with use
  • A hook lives there only if it's used in 2+ places — otherwise inline it
  • Don't pass refs through hook return values unless needed
  • useCallback and useMemo are not free — only use them when measurably needed (memoizing a value that's a child component's prop and feeds a React.memo'd component, or stabilizing a dep for useEffect)

State management

  • Local state: useState, useReducer for complex local flows
  • Server state: TanStack Query — never store fetched data in useState if you'll need to refetch
  • Global state: avoid by default. If needed, use Zustand (small, simple) over Redux Toolkit. Context for low-frequency shared values (theme, locale).
  • Form state: react-hook-form for forms with 3+ fields; controlled inputs with useState for trivial ones

Data fetching with TanStack Query

const { data, isLoading, error } = useQuery({
  queryKey: ['post', id],
  queryFn: () => api.getPost(id),
})
  • Query keys: arrays starting with the resource name; include all params that affect the query
  • Use useMutation for POST/PUT/DELETE; call queryClient.invalidateQueries on success
  • Set staleTime deliberately on each query — defaulting to 0 thrashes the network

Patterns to follow

  • Composition over abstraction — duplicate twice before extracting
  • Push useState down to the component that owns it; lift only when necessary
  • Use <Suspense> boundaries around lazy components and data-fetching components
  • Render lists with stable, content-derived keys (never the array index unless the list is truly static)
  • Keep effects small — one effect per concern, not one giant useEffect

Patterns to avoid

  • Class components. Use hooks.
  • any. Use unknown and narrow.
  • Default exports. Named only.
  • Putting fetched data in useState + useEffect. Use TanStack Query or Server Actions instead.
  • useEffect for derived state. If you can compute it from existing state, just compute it in render.
  • useMemo for everything. It's a cost. Profile first.
  • Inline objects/arrays as props for memoized components. They re-render anyway.
  • Mutating state directly. Always create a new object/array.

Testing

  • Vitest + @testing-library/react
  • Test behavior, not implementation — query by accessible name, not test IDs (unless unavoidable)
  • Mock at the boundary (mock fetch, mock the API client) — not internal helpers
  • E2E with Playwright for critical paths only

Tooling

  • pnpm dev — Vite dev server
  • pnpm build — production build
  • pnpm test — Vitest
  • pnpm lint — ESLint
  • pnpm typechecktsc --noEmit

AI behavioral rules

  • Default to functional components and hooks; never propose a class component
  • Use named exports; never default exports
  • For server data, default to TanStack Query — don't manage server data in useState
  • Don't add useMemo / useCallback unless you can justify the cost
  • Don't add useEffect for things that can be computed during render
  • Validate API responses at the boundary (Zod) — don't trust the server's shape
  • Run lint, typecheck, and tests before declaring a task complete

Frequently asked

How do I use this React rules file with Cursor?

Pick "Cursor (.cursor/rules/*.mdc)" from the format dropdown above and click Copy. Save it at .cursor/rules/main.mdc in your project root and restart Cursor. The legacy .cursorrules format still works if you're on an older Cursor version — pick that option instead.

Can I use this with Claude Code (CLAUDE.md)?

Yes — pick "Claude Code (CLAUDE.md)" from the format dropdown above and copy. Save the file as CLAUDE.md at your repo root. Claude Code reads it automatically on every session. For monorepos, you can also drop nested CLAUDE.md files in subdirectories — Claude merges them when working in those paths.

Where exactly do I put this file?

It depends on the AI tool. Cursor reads .cursorrules or .cursor/rules/*.mdc at the project root. Claude reads CLAUDE.md at the project root. Copilot reads .github/copilot-instructions.md. The "Save at" path under each format in the dropdown shows the exact location for the format you picked.

Can I customize these React rules for my project?

Yes — that's what the generator is for. Click "Open in generator" above and the wizard loads with this stack's defaults pre-selected. Toggle on or off the conventions you want, then re-export in your AI tool's format.

Will using this rules file slow down my AI tool?

No. Rules files count toward the model's context window but not toward latency in any noticeable way. The file is loaded once per session, not per token. The library files target 250–400 lines, well within every tool's recommended budget.

Should I commit this file to git?

Yes. The rules file is project documentation that benefits every developer using the AI tool. Commit it. The exception is personal-global settings (e.g. ~/.claude/CLAUDE.md) which are user-scoped and stay out of the repo.

Related stacks