OTFotf
All posts
General

Introducing: React Best Practices

V
VercelAuthor
12 min
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.

1

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.

2

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.

3

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.

4

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.

More resources

On this page