CLAUDE.md for React: AI-Friendly Frontend Architecture
React gives you infinite flexibility. That's a gift and a curse — especially when you let AI agents loose in your codebase. Without clear boundaries, Claude Code will scatter state across components, put API calls in useEffect chains, and drill props five levels deep. Here's how to structure a React project so AI agents stay in bounds and your code stays clean.
Why React Projects Break With AI Agents
React's flexibility is intentional — it lets you structure your app however you want. But AI agents see this as a feature, not a constraint. Without explicit rules, Claude Code will:
Put async data fetching directly in useEffect, creating waterfalls and race conditions. Add global state in zustand for something that should be local. Drill props through four levels of components instead of using composition. Create a new custom hook for every component instead of centralizing logic. Mix API calls, business logic, and UI rendering in the same component.
Each decision is individually defensible. Together, they create a codebase that's impossible to reason about. The fix isn't smarter AI — it's clearer rules. You need a CLAUDE.md that tells the agent exactly how React should be written in your project.
Feature-Based Architecture for React
React projects should be organized by feature, not by type. Instead of a structure like:
src/components/ src/hooks/ src/stores/ src/api/
Use:
src/features/auth/ src/features/dashboard/ src/features/settings/
Each feature is self-contained. It owns its components, hooks, state management, API calls, and types. This prevents prop drilling across feature boundaries and makes it obvious where state lives.
Your CLAUDE.md should specify the exact directory structure. For example:
features/{feature}/
├── components/ # UI components only
├── hooks/ # Feature-specific custom hooks
├── store.ts # Zustand store (if needed)
├── api.ts # TanStack Query hooks only
├── types.ts # Types and interfaces
└── index.ts # Public exports
This structure forces the agent to keep related code together. It can't pull a component from auth into dashboard without going through the public API. It can't scatter state across multiple files because there's only one store.ts per feature.
State Management Rules: When to Use What
Deciding where state lives is where React projects usually go sideways. Your CLAUDE.md must be explicit about this. Here's a decision tree:
useState: Component-local state only. Form inputs, toggles, UI visibility. Never for async data or multi-component state. Rule in code-patterns.md: "No lifting state up more than one level. Use composition or context instead."
TanStack Query (React Query): Async server data. Every API call must go through useQuery or useMutation. All caching, refetching, and invalidation happens here — never in useState or useEffect. This prevents waterfalls and race conditions.
Zustand: Feature-level state that multiple components share. AuthStore holds the current user. DashboardStore holds filters and sort order. One store per feature. Never global state for temporary UI state.
Context: Only for dependency injection (themes, feature flags, client instances). Never for application state. Context is good for "how do I access this thing", not "what is the state of this thing".
In your CLAUDE.md, include a rule like: "Use React Query for async data. Use Zustand for feature state. Use useState only for component-local UI state. Never mix these — do not store async data in useState or state in useEffect."
Then in code-patterns.md, show concrete examples of correct vs. incorrect patterns. The agent will follow them exactly.
Component Patterns: Smart vs. Presentational
Every component should be either smart or presentational, never both. This is the single most important React pattern for keeping AI agents on track.
Presentational components are pure. They accept props, render UI, and call props-based callbacks. No hooks (except useCallback). No imports from your app — only from UI libraries. They're testable in isolation and easy to understand.
Smart components handle data fetching, state management, and business logic. They use hooks, query the store, call APIs, and compose presentational components. They rarely render UI directly — they orchestrate.
Mixing these creates the code AI agents write by default. It's seductive because it feels efficient — why have two components when one does it all? But it destroys composability and testability.
In code-patterns.md, show the correct pattern with example code:
// ✓ Presentational component
export function UserCard({ user, onEdit, onDelete }: {
user: User;
onEdit: (id: string) => void;
onDelete: (id: string) => void;
}) {
return (
<div>
<h3>{user.name}</h3>
<button onClick={() => onEdit(user.id)}>Edit</button>
<button onClick={() => onDelete(user.id)}>Delete</button>
</div>
);
}
// ✓ Smart component
export function UserListContainer() {
const { data, isLoading } = useUsers();
const deleteMutation = useDeleteUser();
const navigate = useNavigate();
if (isLoading) return <Spinner />;
return (
<div>
{data?.map(user => (
<UserCard
key={user.id}
user={user}
onEdit={(id) => navigate(`/edit/$${id}`)}
onDelete={(id) => deleteMutation.mutate(id)}
/>
))}
</div>
);
}
This pattern forces a clear separation. Presentational components are obvious — they have no hooks (except useCallback). Smart components are obvious — they're usually containers or pages that orchestrate data and UI. The agent will follow this pattern because it's clear.
API Integration: The React Query Way
React Query (TanStack Query) is non-negotiable in your CLAUDE.md. Every API call must go through it. This prevents the waterfall and race condition bugs that AI agents introduce.
In your code-patterns.md, show the pattern:
// features/auth/api.ts
import { useQuery, useMutation } from '@tanstack/react-query';
export function useLogin() {
return useMutation({
mutationFn: async (credentials: LoginInput) => {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials),
});
if (!res.ok) throw new Error('Login failed');
return res.json();
},
onSuccess: (data) => {
authStore.setUser(data.user);
queryClient.invalidateQueries({ queryKey: ['me'] });
},
});
}
Key rules: Every API call is a custom hook. Every hook uses useQuery or useMutation. Data fetching logic never lives in useEffect. Cache invalidation happens in onSuccess or onError callbacks, not scattered throughout the component. The store updates happen in React Query callbacks, keeping them in sync.
Get the free React CLAUDE.md template
Enterprise-grade conventions for every major stack, plus Claude Code and prompt engineering guides. No account needed.
Form Handling: React Hook Form + Zod
Forms are where AI agents usually get lost in a maze of onChange handlers and validation logic. Use React Hook Form + Zod for consistency.
// features/auth/components/LoginForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export function LoginForm() {
const { register, handleSubmit, formState } =
useForm({
resolver: zodResolver(loginSchema),
});
const loginMutation = useLogin();
const onSubmit = (data: LoginInput) => {
loginMutation.mutate(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{formState.errors.email && (
<span>{formState.errors.email.message}</span>
)}
<button disabled={loginMutation.isPending}>
Login
</button>
</form>
);
}
This pattern is bulletproof. Validation is declarative and centralized. The form component is a thin wrapper. The mutation handles submission. The agent will follow this structure every time.
Zustand Store Pattern
Zustand is lightweight and explicit. Show the pattern in code-patterns.md:
// features/auth/store.ts
import { create } from 'zustand';
interface AuthStore {
user: User | null;
setUser: (user: User | null) => void;
logout: () => void;
}
export const useAuthStore = create<AuthStore>((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
});
Rule: One store per feature. Store methods are synchronous — side effects (API calls) happen in React Query, then update the store. No async logic in the store. No re-exporting the store from index.ts — always import directly from store.ts.
The Critical CLAUDE.md Sections for React
Your CLAUDE.md for a React project must include these sections:
Feature-based architecture. Exactly how directories are organized. One feature, one directory. List the required files in each feature folder.
State management decision tree. When to use useState, React Query, Zustand, Context. Be specific: "useState for form inputs under 50 lines. Never for data fetching. Never for multi-component state."
Component rules. "All components are either smart or presentational, never both. Presentational components have no hooks except useCallback. Smart components usually live in containers/ subdirectory."
API integration. "Every API call goes through React Query. Custom hooks in features/{feature}/api.ts. No async logic in components or useEffect."
Naming conventions. Component names, hook names, store names. Example: "Components: PascalCase. Hooks: camelCase, prefix with 'use'. Stores: camelCase, 'useXyzStore'."
Testing patterns. How to test components, hooks, and stores. Example: "Components tested with React Testing Library. Test user behavior, not implementation. No test doubles for components unless necessary."
CLAUDE.md sets the rules. Archie runs the workflow.
Persistent memory, role-based skills, and approval gates. From idea to merged PR.
Getting Started
If you're building a React app (Vite or Next.js), create a CLAUDE.md at your project root today. It doesn't have to be perfect — start with the basics: point to a memory system, define your feature directory structure, and list your state management rules.
Then create a code-patterns.md that shows correct and incorrect examples for the patterns that matter in your app. This single document will save you hours of code review and re-explanation.
Use the free CLAUDE.md template to get started, or customize it for React. The structure is the same across all projects — the key is specificity. Don't say "use React best practices." Say "present components in components/, smart components in containers/, state in features/x/store.ts, API hooks in features/x/api.ts."
With clear boundaries, AI agents will stay in them. Your React codebase will be cleaner, easier to navigate, and less painful to maintain.