React Guide
Applies to: React 18+, TypeScript, Single Page Applications, Component Libraries, Web Apps
Core Principles
-
Component-Based: Build encapsulated components that manage their own state
-
Declarative: Describe what UI should look like, React handles DOM updates
-
Unidirectional Data Flow: Props down, callbacks up
-
Composition Over Inheritance: Prefer component composition and custom hooks
-
TypeScript First: All components use TypeScript with strict prop interfaces
Guardrails
Component Rules
-
Use functional components exclusively (no class components)
-
Keep components under 200 lines (split into smaller components)
-
Every component has a TypeScript interface for props
-
Use React.ReactNode for children props, not JSX.Element
-
Implement error boundaries for graceful error handling
-
Use proper accessibility attributes (ARIA roles, labels, keyboard navigation)
-
Co-locate component, styles, and tests in the same directory
Hooks Rules
-
Only call hooks at the top level (never inside conditions, loops, or nested functions)
-
Only call hooks from React function components or custom hooks
-
Name custom hooks with use prefix: useAuth , useQuery , useFetch
-
Always include all dependencies in useEffect / useMemo / useCallback arrays
-
Clean up side effects in useEffect return function
-
Prefer useReducer over useState for complex state logic
-
Use ESLint react-hooks plugin to enforce rules
State Management
-
Local state: useState for simple, useReducer for complex
-
Shared state: Context API for low-frequency updates (theme, auth, locale)
-
Server state: TanStack Query (React Query) for data fetching and caching
-
Global client state: Zustand for complex cross-component state
-
Avoid prop drilling beyond 2 levels (use composition or context)
-
Never store derived state (compute it during render)
Performance
-
Use React.memo() only for expensive pure components (measure first)
-
Use useMemo for expensive computations, useCallback for stable references
-
Lazy load routes and heavy components with React.lazy()
- Suspense
-
Virtualize long lists (react-window, @tanstack/virtual)
-
Keep bundle under 200KB initial load (code-split aggressively)
-
Avoid anonymous functions in JSX for frequently re-rendered components
File Naming
-
Components: PascalCase.tsx (e.g., UserCard.tsx )
-
Hooks: camelCase.ts with use prefix (e.g., useAuth.ts )
-
Utils: camelCase.ts (e.g., formatDate.ts )
-
Types: camelCase.ts or types.ts per feature
-
Tests: *.test.tsx or *.spec.tsx (co-located)
-
Styles: *.module.css or *.styles.ts (co-located)
Project Structure
my-app/ ├── public/ │ └── index.html ├── src/ │ ├── assets/ # Static assets (images, fonts) │ ├── components/ # Reusable components │ │ ├── ui/ # Base UI components (Button, Input, Modal) │ │ └── features/ # Feature-specific components │ ├── hooks/ # Custom hooks │ ├── contexts/ # React contexts │ ├── services/ # API services │ ├── utils/ # Utility functions │ ├── types/ # TypeScript types │ ├── pages/ # Page components (route-level) │ ├── App.tsx │ ├── main.tsx │ └── index.css ├── tests/ ├── package.json ├── tsconfig.json ├── vite.config.ts └── README.md
-
components/ui/ for reusable design system primitives
-
components/features/ for domain-specific compositions
-
hooks/ for shared custom hooks (feature-specific hooks co-locate with component)
-
contexts/ for React context providers
-
services/ for API layer (fetch wrappers, API clients)
-
pages/ for route-level components only
Component Patterns
Functional Component with Props
interface UserCardProps { userId: string; onSelect?: (user: User) => void; className?: string; }
export function UserCard({ userId, onSelect, className }: UserCardProps) { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(true);
useEffect(() => { const fetchUser = async () => { try { const data = await userService.getById(userId); setUser(data); } finally { setLoading(false); } }; fetchUser(); }, [userId]);
if (loading) return <Skeleton />; if (!user) return null;
return ( <div className={cn('user-card', className)} onClick={() => onSelect?.(user)}> <Avatar src={user.avatar} alt={user.name} /> <h3>{user.name}</h3> <p>{user.email}</p> </div> ); }
Component with Children
interface CardProps { title: string; children: React.ReactNode; footer?: React.ReactNode; }
export function Card({ title, children, footer }: CardProps) { return ( <div className="card"> <div className="card-header"><h2>{title}</h2></div> <div className="card-body">{children}</div> {footer && <div className="card-footer">{footer}</div>} </div> ); }
State Management Patterns
Context API (Auth, Theme, Locale)
interface AuthContextValue { user: User | null; login: (credentials: LoginCredentials) => Promise<void>; logout: () => void; isAuthenticated: boolean; }
const AuthContext = createContext<AuthContextValue | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<User | null>(null);
const login = async (credentials: LoginCredentials) => { const user = await authService.login(credentials); setUser(user); };
const logout = () => { authService.logout(); setUser(null); };
return ( <AuthContext.Provider value={{ user, login, logout, isAuthenticated: !!user }}> {children} </AuthContext.Provider> ); }
export function useAuth() { const context = useContext(AuthContext); if (!context) throw new Error('useAuth must be used within AuthProvider'); return context; }
TanStack Query (Server State)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function useUsers() { return useQuery({ queryKey: ['users'], queryFn: () => userService.getAll(), staleTime: 5 * 60 * 1000, }); }
function useCreateUser() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (data: CreateUserDTO) => userService.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }); }, }); }
Zustand (Complex Client State)
import { create } from 'zustand';
interface CartStore { items: CartItem[]; addItem: (item: CartItem) => void; removeItem: (id: string) => void; clearCart: () => void; total: () => number; }
const useCartStore = create<CartStore>()((set, get) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), removeItem: (id) => set((state) => ({ items: state.items.filter((item) => item.id !== id), })), clearCart: () => set({ items: [] }), total: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0), }));
Routing (React Router v6)
import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard')); const Settings = lazy(() => import('./pages/Settings'));
const router = createBrowserRouter([ { path: '/', element: <Layout />, children: [ { index: true, element: <Home /> }, { path: 'dashboard', element: <Dashboard /> }, { path: 'settings', element: <Settings /> }, { path: '*', element: <NotFound /> }, ], }, ]);
function App() { return ( <Suspense fallback={<LoadingSpinner />}> <RouterProvider router={router} /> </Suspense> ); }
Forms (React Hook Form + Zod)
import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod';
const userSchema = z.object({ email: z.string().email('Invalid email'), password: z.string().min(8, 'Password must be at least 8 characters'), name: z.string().min(2, 'Name is required'), });
type UserFormData = z.infer<typeof userSchema>;
function UserForm({ onSubmit }: { onSubmit: (data: UserFormData) => void }) { const { register, handleSubmit, formState: { errors, isSubmitting }, reset, } = useForm<UserFormData>({ resolver: zodResolver(userSchema) });
const handleFormSubmit = async (data: UserFormData) => { await onSubmit(data); reset(); };
return ( <form onSubmit={handleSubmit(handleFormSubmit)}> <div> <label htmlFor="email">Email</label> <input id="email" type="email" {...register('email')} /> {errors.email && <span className="error">{errors.email.message}</span>} </div> <button type="submit" disabled={isSubmitting}> {isSubmitting ? 'Submitting...' : 'Submit'} </button> </form> ); }
Testing Overview
Standards
-
Use React Testing Library (@testing-library/react ) for component tests
-
Use userEvent over fireEvent for realistic user interactions
-
Test behavior, not implementation details
-
Query by accessible roles and labels first (not test IDs)
-
Mock API calls, not internal component state
-
Coverage target: >80% for business logic components
Basic Component Test
import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event';
describe('UserCard', () => { it('renders user information', () => { render(<UserCard user={mockUser} />); expect(screen.getByText('John Doe')).toBeInTheDocument(); });
it('calls onSelect when clicked', async () => { const onSelect = vi.fn(); render(<UserCard user={mockUser} onSelect={onSelect} />); await userEvent.click(screen.getByRole('button')); expect(onSelect).toHaveBeenCalledWith(mockUser); }); });
Tooling
Essential Commands
npm create vite@latest my-app -- --template react-ts # New project npm install # Install deps npm run dev # Dev server npm run build # Production build npm run lint # ESLint npm test # Run tests npm run test:cov # Coverage
Recommended Dependencies
{ "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.x", "@tanstack/react-query": "^5.x", "zustand": "^4.x", "react-hook-form": "^7.x", "zod": "^3.x", "@hookform/resolvers": "^3.x" }, "devDependencies": { "@types/react": "^18.x", "@types/react-dom": "^18.x", "@testing-library/react": "^14.x", "@testing-library/user-event": "^14.x", "vitest": "^1.x", "eslint-plugin-react-hooks": "^4.x" } }
ESLint Configuration
{ "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended", "plugin:react-hooks/recommended" ], "rules": { "react/react-in-jsx-scope": "off", "react/prop-types": "off", "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn" } }
Advanced Topics
For detailed patterns and examples, see:
- references/patterns.md -- Compound components, custom hooks, testing, performance, security patterns
External References
-
React Documentation
-
React TypeScript Cheatsheet
-
TanStack Query
-
Zustand
-
React Testing Library
-
React Hook Form
-
Zod