Introducing: React Best Practices
A tutorial by Vercel. Featured in the OTF curated resource library.
Why Practices Matter More with AI
When AI generates code, it follows patterns. Without explicit conventions, it follows average patterns — the statistical mean of all React code it was trained on. This produces code that works but lacks cohesion.
With explicit best practices, AI follows your patterns — producing code that's consistent, maintainable, and aligned with your project's architecture. The ROI of establishing React conventions is 10x higher with AI tools than without them.
These aren't theoretical best practices. They're battle-tested patterns that produce the best results when used with AI coding agents like Cursor, Claude Code, and Copilot.
Component Design Patterns
Single Responsibility Components
Each component does one thing. A UserCard displays user info. A UserList fetches and lists users. A UserPage composes them with layout. AI produces cleaner code when responsibility boundaries are clear.
Composition Over Configuration
Prefer composable components (children, render props) over heavily configured ones (20+ props). AI understands composition patterns better and generates more flexible components.
150-Line Maximum
If a component exceeds 150 lines, split it. Extract custom hooks for logic, sub-components for UI sections, and utility functions for transformations. AI tools manage smaller files more accurately.
Named Exports Only
Always use named exports: `export function UserCard()`. Never default exports. Named exports make imports explicit, help AI tools find references, and prevent naming ambiguity across files.
State Management Strategy
State management is where AI-generated code diverges most. Without conventions, you'll get a mix of useState, useReducer, useContext, and external libraries — often in the same file.
The clear strategy:
- Server state: React Query (TanStack Query) for ALL data fetching. Never useState + useEffect for API calls. React Query handles caching, refetching, loading states, and error states.
- UI state: useState for component-local state (open/closed, selected item, form inputs). Keep it close to where it's used.
- URL state: React Router for state that should survive navigation (search queries, filters, pagination). Use URL search params, not component state.
- Global UI state: Context for cross-cutting concerns (theme, authentication, notifications). Avoid Zustand/Redux unless the app is truly complex.
Rule of thumb: if the state comes from an API, use React Query. If it's UI-only, use useState. If it's in the URL, use the router. If it's shared across distant components, use Context.
Data Fetching Patterns
Consistent data fetching is the single biggest quality improvement for AI-generated React code.
Custom hooks for every data source
Create a custom hook for each data entity: useProjects(), useTutorials(), useTools(). Each hook encapsulates the React Query configuration, type definitions, and data transformation.
Type everything
Define TypeScript interfaces for all API responses and transformed data. AI tools use these types to generate correct component props, list renderings, and conditional displays.
Handle all states
Every data-fetching component handles three states: loading (skeleton/spinner), error (user-friendly message with retry), and success (the actual content). AI generates all three when the pattern is established.
Optimistic updates
For mutations (create, update, delete), update the UI immediately and let the API call happen in the background. If it fails, revert. This pattern is worth encoding in your conventions — it dramatically improves UX.
File Organization
A clear file structure is the best signal you can give AI tools. When files are predictably organized, AI generates code in the right location with the right imports.
Recommended structure:
``
src/
├── pages/ # One file per route
├── components/
│ ├── ui/ # Design system primitives
│ ├── shared/ # Reusable business components
│ ├── layout/ # Navbar, Footer, Sidebar
│ └── {feature}/ # Feature-specific components
├── hooks/ # Custom React hooks
├── lib/ # Utilities, API client, helpers
├── types/ # Shared TypeScript types
└── data/ # Static data, constants
Naming conventions:
- Files: PascalCase for components (UserCard.tsx), camelCase for hooks (useProjects.ts)
- Components: Match filename (export function UserCard())
- Hooks: Always start with use (useAuth, useProjects)
- Types: Interfaces for objects, types for unions/primitives
AI-Specific Conventions
Explicit Import Paths
Use complete relative paths in imports: `from '../hooks/useProjects'` not `from '@/hooks/useProjects'`. AI tools handle relative paths more reliably across different project configurations.
Co-locate Types with Usage
Define component prop types in the same file as the component. Define hook return types in the hook file. AI reads the file it's editing — co-located types are always visible.
Comments for Non-Obvious Patterns
When your code does something unexpected (SQLite booleans as '0'/'1' strings, camelCase-to-snake_case conversion), add a brief comment. AI reads comments and avoids regenerating the pattern incorrectly.
Consistent Error Handling
Use one error handling pattern everywhere: try/catch with toast notifications for user errors, console.error for debug logging. AI replicates the pattern it sees most often in your codebase.