agentleFS
base58edbase58ed/claude_code_files/skills/react-skill/SKILL.md

react

Skill1 starsChanged 3 months agoMITView on GitHub

React + TypeScript frontend patterns. ALWAYS load for .tsx/.jsx files, React components, Vite projects, TanStack Query, CSS Modules, or any React-related task. Trigger vocabulary includes React Router, Vitest, Testing Library, React Hook Form, Zod, frontend components, hooks — even when the user doesn't say "React" explicitly.

  • Installs packages

48 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use app-renderer-systems, refactoring-analysis and 8 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • pedronauckSkill

    app-renderer-systems

    Create, extend, or debug frontend domain modules organized under systems/. Covers adapters, Query hooks, local state, and public exports; excludes generic components and backend implementation.

    Also in 3 projects that use this

  • pedronauckSkill

    refactoring-analysis

    Audit code quality and architectural health or plan refactoring using Fowler code smells and techniques; write prioritized findings to docs/_refacs/. Excludes formatting, performance, and security audits.

    Also in 3 projects that use this

  • andyridsSkill

    express

    Run the ICM express-change pipeline - a small change no spec has to move for, with the plan record left intact

    Also in 4 projects that use this

  • azlekovSkill

    tailwindcss

    This skill should be used when the user asks to "style with Tailwind", "add CSS", "configure theme", "use @theme", "add custom colors", "implement dark mode", "use container queries", "add responsive design", "use OKLCH colors", or discusses styling, theming, or visual design. Always use the latest Tailwind CSS version and modern patterns.

    Also in 5 projects that use this

  • jander99Skill

    typescript-advanced

    Design, implement, refactor, and debug advanced TypeScript patterns including generics, utility types, mapped types, conditional types, type guards, discriminated unions, and strict mode configuration. Use when writing type-safe code, fixing type errors, or improving type inference.

    Also in 3 projects that use this

  • rawvegSkill

    laravel

    Laravel v12 - The PHP Framework For Web Artisans

    Also in 6 projects that use this

The file

We split this skill into the rules it gives an agent

11 rules. 0 are common, 6 are distinctive, and 5 are very specific to this project.

  • 0

    Common

    Most of the 38 edited versions we read keep these.

  • 6

    Distinctive

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Delete aggressively — git preserves history

    line 21
  2. Distinctive:

    Colocation rule: If it's used by one component, it lives next to that component.

    line 60
  3. Distinctive:

    No default exports — named exports are greppable and refactor-safe

    line 104
  4. Distinctive:

    All magic values come from CSS custom properties — no hardcoded colors, spacing, or font sizes in component CSS

    line 190
  5. Very specific:

    Every entity gets a key factory.

    line 241
  6. Distinctive:

    No state for values derivable from other state — compute inline

    line 270
  7. Very specific:

    Keep the work plan out of comments — no phases, plan steps, ticket numbers, or words like "for now", "temporary", "later".

    line 272
  8. Distinctive:

    Behavioral over interaction testing: Test what the user sees and does, not implementation details.

    line 327
  9. Very specific:

    Before committing or completing a review, read references/checklist.md in this skill's directory for the full checklist.

    line 432
  10. Very specific:

    All props interfaces have readonly — no accidental mutation

    line 434
  11. Very specific:

    No derived state in useState — compute inline

    line 437

“Common” is measured: kept by at least 6 in 10 of the 38 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Version Policy

React 19+, TypeScript 6+, Vite 8+. React Compiler 1.0 (stable) handles memoization automatically — no manual useMemo/useCallback needed. ref is a regular prop — no forwardRef. Verify feature availability when uncertain.

Philosophy

  • Components are functions — pure transforms from props + state to UI
  • Composition over configuration — small components that compose, pass children instead of drilling props, no god components
  • Colocation — styles, tests, types live next to the component they serve
  • Server state is not UI state — TanStack Query for remote data, useState for UI
  • Shared client state (theme, session, cart): lift state first, then a small typed context at the nearest boundary — this skill's default is no global state library (Redux, zustand) without measured prop-drilling pain
  • Type-driven development — if the type is wrong, the UI is wrong
  • Zero-runtime styling — CSS Modules, no CSS-in-JS runtime overhead
  • Render only what changed — stable references, correct dependency arrays, no wasted renders
  • Explicit over magic — no HOC chains, no deep context nesting, obvious data flow
  • Delete aggressively — git preserves history
  • Measure before optimizing — React DevTools Profiler, not intuition

Project Structure

src/
├── main.tsx                  # Entry — React.createRoot, QueryClient, Router
├── app.tsx                   # Root layout + route definitions
├── routes/                   # Page components (1:1 with routes)
│   ├── home.tsx
│   └── users/
│       ├── list.tsx
│       └── detail.tsx
├── components/               # Shared UI components
│   ├── button/
│   │   ├── button.tsx
│   │   ├── button.module.css
│   │   └── button.test.tsx
│   └── layout/
├── features/                 # Feature-scoped modules (components + hooks + types)
│   └── orders/
│       ├── components/
│       ├── hooks/
│       │   └── use-orders.ts
│       ├── types.ts
│       └── index.ts          # Public API barrel
├── hooks/                    # Shared custom hooks
├── lib/                      # Utilities, API client, result type
│   ├── api-client.ts
│   ├── result.ts
│   ├── invariant.ts          # cheap runtime asserts
│   └── cn.ts                 # classnames utility
├── types/                    # Shared types
└── styles/
    ├── reset.css
    └── variables.css          # CSS custom properties (design tokens)

Colocation rule: If it's used by one component, it lives next to that component. If shared, lift to the nearest common ancestor directory.

Invariants: lib/invariant.ts — a cheap assert that throws in all builds, for impossible states: context used outside its provider, exhaustive-switch defaults.

TypeScript Strict Mode

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

noUncheckedIndexedAccess is the most impactful — array/object indexing returns T | undefined, forcing you to handle missing values.

Component Patterns

Function Components — Props as Interface

interface UserCardProps {
  readonly user: User;
  readonly onSelect: (id: string) => void;
}

function UserCard({ user, onSelect }: UserCardProps) {
  return (
    <article className={styles.card} onClick={() => onSelect(user.id)}>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
    </article>
  );
}

Rules:

  • Named function declarations, not const Component = () => {} — better stack traces, hoisting
  • Props interface with readonly — prevents accidental mutation
  • Destructure props in parameter — no props.x drilling
  • No default exports — named exports are greppable and refactor-safe

Discriminated Unions for Variant Props

type ButtonProps =
  | { readonly variant: "primary"; readonly onClick: () => void }
  | { readonly variant: "link"; readonly href: string }
  | { readonly variant: "submit" };

function Button(props: ButtonProps) {
  switch (props.variant) {
    case "primary":
      return <button onClick={props.onClick} className={styles.primary}>Click</button>;
    case "link":
      return <a href={props.href} className={styles.link}>Link</a>;
    case "submit":
      return <button type="submit" className={styles.submit}>Submit</button>;
  }
}

Compiler enforces exhaustiveness — add a variant, get errors everywhere it's unhandled.

Guard Clauses in Components

function UserProfile({ userId }: { readonly userId: string }) {
  const { data: user, isPending, error } = useUser(userId);

  if (isPending) return <Skeleton />;
  if (error) return <ErrorDisplay error={error} />;
  if (!user) return <NotFound />;

  // Happy path — no nesting
  return (
    <article>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </article>
  );
}

Early returns for loading/error/empty states. Happy path is always the last, un-nested return. Use TanStack Query v5's isPending (no data yet) — isLoading is the narrower isPending && isFetching and stays false for disabled queries.

CSS Modules

Zero-runtime, scoped CSS. No abstraction — just CSS with automatic class name isolation.

/* button.module.css */
.root {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-md);
  font-weight: 500;
  transition: background-color 0.15s;
}

.primary {
  composes: root;
  background-color: var(--color-primary);
  color: var(--color-on-primary);
}

.primary:hover {
  background-color: var(--color-primary-hover);
}
import styles from "./button.module.css";

function Button({ variant = "primary", children }: ButtonProps) {
  return <button className={styles[variant]}>{children}</button>;
}

Design Tokens as CSS Custom Properties

At project init, copy assets/variables.css from this skill's directory to src/styles/variables.css (spacing scale, colors, typography, radii, shadows) and extend it in place.

Rules:

  • All magic values come from CSS custom properties — no hardcoded colors, spacing, or font sizes in component CSS
  • composes: for CSS composition — not class name concatenation
  • One .module.css per component — colocated

Data Fetching (TanStack Query)

Server state management — caching, deduplication, background refetch, retry.

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

// Query keys — structured, type-safe
const userKeys = {
  all: ["users"] as const,
  lists: () => [...userKeys.all, "list"] as const,
  list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
  details: () => [...userKeys.all, "detail"] as const,
  detail: (id: string) => [...userKeys.details(), id] as const,
};

// Custom hook — encapsulates query config
function useUser(id: string) {
  return useQuery({
    queryKey: userKeys.detail(id),
    queryFn: () => apiClient.get<User>(`/users/${id}`),
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}

function useUsers(filters: UserFilters) {
  return useQuery({
    queryKey: userKeys.list(filters),
    queryFn: () => apiClient.get<User[]>("/users", { params: filters }),
  });
}

// Mutation with cache invalidation
function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (input: CreateUserInput) => apiClient.post<User>("/users", input),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: userKeys.lists() });
    },
  });
}

Query Key Factory Pattern

Every entity gets a key factory. Structure: [entity, scope, ...params]. This enables precise cache invalidation — invalidateQueries({ queryKey: userKeys.lists() }) invalidates all user lists but not individual user details.

API Client

Thin wrapper over fetch. When creating the API client, read references/api-client.md in this skill's directory for the canonical implementation with interceptors, error handling, and type-safe methods.

// Usage — clean, typed
const user = await apiClient.get<User>("/users/123");
const created = await apiClient.post<User>("/users", { name: "Alice" });

Result Type for API Boundaries

For complex error handling flows where you need to pattern-match on errors (not just display them), read references/result.md in this skill's directory.

For most React components, TanStack Query's built-in { data, error, isPending } is sufficient — you don't need a custom Result type at the component level. Use Result in the API client layer when you need to distinguish error types for business logic.

Hooks

Custom Hooks — Extract Reusable Logic

Encapsulate repeated stateful logic behind a typed signature, e.g. function useDebounce<T>(value: T, delayMs: number): T (setTimeout + cleanup in useEffect).

Rules of Hooks + Performance

  • Extract repeated useState + useEffect combos into custom hooks
  • useMemo / useCallback only when measured — not by default
  • Dependency arrays must be exhaustive — no lint suppressions
  • No state for values derivable from other state — compute inline
  • Comments are for the person reading and reviewing the code: the public API, anything not obvious, and the reason behind the code as a plain sentence (no WHY: label)
  • Keep the work plan out of comments — no phases, plan steps, ticket numbers, or words like "for now", "temporary", "later". They go stale and someone has to find and delete them. Unfinished work belongs in plan.md
  • Any function that does real work gets a header comment above it: one line on what it is for, plus its big-O for compute and memory. Tiny obvious helpers are exempt. The big-O lives in the header, not on a line inside the body:
    // Group orders by customer for the summary table. O(n) compute, O(n) memory.
    function groupByCustomer(orders: readonly Order[]): Map<string, Order[]> { /* ... */ }
  • Non-trivial data transforms (sorts, groupings, derived collections) carry these big-O comments per CLAUDE.md — JSX glue and one-liner filters are exempt
  • Editing counts too: when you change a function, keep the comments already there, fix any your change made wrong, and add the missing header, big-O, and reason. Do this only for code you add or change, not for nearby code you did not touch
// BAD: derived state in useState
const [filteredItems, setFilteredItems] = useState<Item[]>([]);
useEffect(() => {
  setFilteredItems(items.filter(i => i.active));
}, [items]);

// GOOD: compute inline — no state, no effect, no extra render
const filteredItems = items.filter(i => i.active);

Performance

  • React Compiler handles memoization — do NOT write manual useMemo, useCallback, or React.memo. The compiler 1.0 auto-memoizes components, hooks, and expressions. Writing manual memoization on top of the compiler is redundant and adds noise.
  • Don't optimize prematurely — React is fast. Measure with DevTools Profiler first.
  • Virtualize long lists — @tanstack/react-virtual for lists > 100 items
  • Code split routes — React.lazy() + Suspense for route-level splitting
  • Images — lazy loading (loading="lazy"), proper width/height to prevent layout shift
// Route-level code splitting
const UserList = lazy(() => import("./routes/users/list"));
const UserDetail = lazy(() => import("./routes/users/detail"));

<Suspense fallback={<Skeleton />}>
  <Routes>
    <Route path="/users" element={<UserList />} />
    <Route path="/users/:id" element={<UserDetail />} />
  </Routes>
</Suspense>

Error Boundaries

The one sanctioned class component — React has no hook API for getDerivedStateFromError / componentDidCatch. Wrap route-level components. Don't wrap every small component — boundaries should be at meaningful recovery points.

Forms (Controlled + Validation)

For simple forms, controlled inputs with useState. For complex forms with validation, read references/forms.md in this skill's directory for React Hook Form patterns.

React 19 Actions stance: mutations go through TanStack Query useMutation (cache invalidation lives there), not useActionState. Form state is useState or React Hook Form per references/forms.md — do not mix in <form action> / form actions.

Testing (Vitest + Testing Library)

TDD: red → green → vacuous mutation check. The regression test lands before the fix.

Behavioral over interaction testing. Test what the user sees and does, not implementation details.

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";

describe("UserCard", () => {
  it("displays user name and email", () => {
    render(<UserCard user={{ id: "1", name: "Alice", email: "a@b.com" }} onSelect={() => {}} />);

    expect(screen.getByText("Alice")).toBeInTheDocument();
    expect(screen.getByText("a@b.com")).toBeInTheDocument();
  });

  it("calls onSelect with user id when clicked", async () => {
    const user = userEvent.setup();
    const onSelect = vi.fn();

    render(<UserCard user={{ id: "1", name: "Alice", email: "a@b.com" }} onSelect={onSelect} />);
    await user.click(screen.getByRole("article"));

    expect(onSelect).toHaveBeenCalledWith("1");
  });
});

Testing TanStack Query Hooks

Stub the API boundary — hook tests must never hit the network. Spy on the apiClient singleton (lighter than a mock server; reach for MSW only in full integration suites).

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { renderHook, waitFor } from "@testing-library/react";
import { vi } from "vitest";
import { apiClient } from "@/lib/api-client";

function createWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  return function Wrapper({ children }: { readonly children: React.ReactNode }) {
    return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
  };
}

it("fetches user by id", async () => {
  vi.spyOn(apiClient, "get").mockResolvedValue({ id: "123", name: "Alice", email: "a@b.com" });

  const { result } = renderHook(() => useUser("123"), { wrapper: createWrapper() });
  await waitFor(() => expect(result.current.isSuccess).toBe(true));
  expect(result.current.data?.name).toBe("Alice");
});

Anti-Patterns

// BAD: Testing implementation details
expect(component.state.isOpen).toBe(true);

// BAD: Querying by class name or test ID when better queries exist
screen.getByTestId("submit-btn");

// GOOD: Query by role, label, or text — what the user sees
screen.getByRole("button", { name: "Submit" });
screen.getByLabelText("Email");
screen.getByText("Welcome back");

Query priority: getByRole > getByLabelText > getByText > getByTestId (last resort).

Dependencies

Before adding a dependency, check if it's already provided transitively: npm ls <package>. Duplicate direct dependencies cause version conflicts and lock file churn.

# Core
npm create vite@latest project-name -- --template react-ts
npm i react-router @tanstack/react-query

# React Compiler (auto-memoization — replaces manual useMemo/useCallback)
npm i -D babel-plugin-react-compiler @rolldown/plugin-babel @babel/core @types/babel__core

# Dev
npm i -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event
npm i -D @biomejs/biome
npm i -D @tanstack/react-query-devtools

React Router v7 is a single package in declarative mode — import from "react-router" (react-router-dom is a legacy re-export shim, do not install it).

Enable React Compiler in vite.config.ts (plugin-react v6 removed the inline babel option — use @rolldown/plugin-babel with the exported preset):

import { defineConfig } from "vite";
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
import babel from "@rolldown/plugin-babel";

export default defineConfig({
  plugins: [react(), babel({ presets: [reactCompilerPreset()] })],
});

Before Committing (Self-Review)

Before committing or completing a review, read references/checklist.md in this skill's directory for the full checklist. Quick top-of-mind checks:

  • All props interfaces have readonly — no accidental mutation
  • No any — use unknown and narrow, or fix the type
  • Guard clauses for loading/error/empty — happy path last
  • No derived state in useState — compute inline
  • CSS values from custom properties — no hardcoded colors/spacing
  • TanStack Query keys use factory pattern — precise invalidation
  • Tests query by role/label/text — not implementation details

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.