Best Practices Collection¶
Comprehensive collection of React and Next.js development best practices for 2025.
Project Structure¶
Feature-Based Architecture¶
Recommended structure:
src/
├── app/ # Next.js App Router
│ ├── (auth)/
│ │ ├── login/
│ │ └── register/
│ ├── (dashboard)/
│ │ └── page.tsx
│ └── layout.tsx
├── features/ # Feature modules
│ ├── blog/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── domain/
│ │ ├── usecases/
│ │ └── services/
│ ├── user/
│ └── auth/
├── shared/ # Shared resources
│ ├── ui/ # Atomic design components
│ │ ├── atoms/
│ │ ├── molecules/
│ │ └── organisms/
│ ├── hooks/
│ ├── utils/
│ └── types/
├── core/ # Core business logic
│ ├── domain/
│ ├── repository/
│ └── usecases/
└── store/ # State management
├── uiStore.ts
└── authStore.ts
Naming Conventions¶
| Type | Convention | Example |
|---|---|---|
| Components | PascalCase | UserCard.tsx |
| Hooks | camelCase + "use" prefix | useAuth.ts |
| Utils | camelCase | formatDate.ts |
| Types | PascalCase | User.ts |
| Constants | UPPER_SNAKE_CASE | API_ENDPOINTS.ts |
| Context | PascalCase + "Context" | ThemeContext.tsx |
| Server Actions | camelCase + "Action" | createUserAction.ts |
Component Design¶
Atomic Design Principles¶
Atoms - Basic building blocks:
// Button.tsx
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
onClick?: () => void;
}
export function Button({ variant = 'primary', size = 'md', children, onClick }: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
onClick={onClick}
>
{children}
</button>
);
}
Molecules - Combinations of atoms:
// FormField.tsx
interface FormFieldProps {
label: string;
error?: string;
children: React.ReactNode;
}
export function FormField({ label, error, children }: FormFieldProps) {
return (
<div className="form-field">
<label>{label}</label>
{children}
{error && <span className="error">{error}</span>}
</div>
);
}
Organisms - Complex UI sections:
// LoginForm.tsx
export function LoginForm({ onSubmit }: { onSubmit: (data: LoginData) => void }) {
return (
<form onSubmit={handleSubmit(onSubmit)}>
<FormField label="Username">
<Input {...register('username')} />
</FormField>
<FormField label="Password">
<Input type="password" {...register('password')} />
</FormField>
<Button type="submit">Login</Button>
</form>
);
}
Server vs Client Components¶
Default to Server Components:
// app/posts/page.tsx - Server Component (default)
async function PostsPage() {
const posts = await getPosts(); // Direct data fetching
return (
<div>
<h1>Posts</h1>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
Use Client Components for interactivity:
// components/LikeButton.tsx
"use client";
import { useState } from 'react';
export function LikeButton({ postId }: { postId: number }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
);
}
State Management¶
Choosing the Right Tool¶
| State Type | Tool | Example |
|---|---|---|
| Server state | React Query / SWR | API data, cached responses |
| Global UI state | Zustand / Jotai | Theme, sidebar open/closed |
| Form state | React Hook Form | Input values, validation |
| URL state | Next.js searchParams | Filters, pagination |
| Component state | useState | Local toggles, inputs |
Zustand Best Practices¶
// store/uiStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface UiState {
sidebarOpen: boolean;
theme: 'light' | 'dark';
toggleSidebar: () => void;
setTheme: (theme: 'light' | 'dark') => void;
}
export const useUiStore = create<UiState>()(
persist(
(set) => ({
sidebarOpen: false,
theme: 'light',
toggleSidebar: () => set((state) => ({
sidebarOpen: !state.sidebarOpen
})),
setTheme: (theme) => set({ theme }),
}),
{
name: 'ui-storage',
}
)
);
React Query Patterns¶
// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createUser,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
TypeScript Best Practices¶
Strict Type Safety¶
tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
}
}
Type Definitions¶
// types/user.ts
export interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
createdAt: Date;
}
// Utility types
export type UserCreateInput = Omit<User, 'id' | 'createdAt'>;
export type UserUpdateInput = Partial<UserCreateInput>;
export type UserPublic = Pick<User, 'id' | 'name'>;
// Branded types for IDs
type Brand<K, T> = K & { __brand: T };
export type UserId = Brand<number, 'UserId'>;
export type PostId = Brand<number, 'PostId'>;
Generic Components¶
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={keyExtractor(item)}>
{renderItem(item)}
</li>
))}
</ul>
);
}
// Usage
<List
items={users}
renderItem={(user) => <UserCard user={user} />}
keyExtractor={(user) => user.id}
/>
Error Handling¶
Error Boundaries¶
// components/ErrorBoundary.tsx
'use client';
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: any) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div>
<h2>Something went wrong</h2>
<details>
<summary>Error details</summary>
<pre>{this.state.error?.message}</pre>
</details>
</div>
);
}
return this.props.children;
}
}
Next.js Error Handling¶
app/error.tsx:
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
app/global-error.tsx:
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<h2>Application Error</h2>
<button onClick={reset}>Reset</button>
</body>
</html>
);
}
Security Best Practices¶
Input Sanitization¶
import DOMPurify from 'isomorphic-dompurify';
// Sanitize user input before rendering
function UserComment({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
Environment Variables¶
// lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
NEXTAUTH_URL: z.string().url(),
API_KEY: z.string(),
});
export const env = envSchema.parse({
DATABASE_URL: process.env.DATABASE_URL,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
API_KEY: process.env.API_KEY,
});
// Usage
const url = env.DATABASE_URL; // Type-safe!
Server Actions Security¶
"use server";
import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
export async function deletePost(postId: number) {
const session = await auth();
// Verify authentication
if (!session?.user) {
redirect('/login');
}
// Verify authorization
const post = await db.post.findUnique({ where: { id: postId } });
if (post.authorId !== session.user.id) {
throw new Error('Unauthorized');
}
// Perform action
await db.post.delete({ where: { id: postId } });
}
Performance Patterns¶
Lazy Loading¶
import dynamic from 'next/dynamic';
// Lazy load heavy components
const Chart = dynamic(() => import('./Chart'), {
loading: () => <ChartSkeleton />,
ssr: false, // Client-only
});
// Lazy load with delay
const Modal = dynamic(() => import('./Modal'), {
loading: () => <Spinner />,
});
Memoization Patterns¶
// Memoize expensive computations
const sortedData = useMemo(
() => data.sort((a, b) => a.value - b.value),
[data]
);
// Memoize callbacks passed to children
const handleClick = useCallback((id: number) => {
console.log('Clicked:', id);
}, []);
// Memoize entire component
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) {
return <div>{/* complex rendering */}</div>;
});
Image Optimization¶
import Image from 'next/image';
// Optimized images
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // Above fold
placeholder="blur"
blurDataURL="data:image/..."
/>
// Responsive images
<Image
src="/photo.jpg"
alt="Photo"
fill
sizes="(max-width: 768px) 100vw, 50vw"
style={{ objectFit: 'cover' }}
/>
Code Quality¶
ESLint Configuration¶
.eslintrc.json:
{
"extends": [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"react/no-unescaped-entities": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ "argsIgnorePattern": "^_" }
],
"react-hooks/exhaustive-deps": "warn",
"no-console": ["warn", { "allow": ["warn", "error"] }]
}
}
Prettier Configuration¶
.prettierrc:
Git Hooks with Husky¶
.husky/pre-commit:
package.json:
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}
}
Accessibility¶
Semantic HTML¶
// ✅ Good - semantic
<nav>
<ul>
<li><a href="/home">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
// ❌ Bad - non-semantic
<div className="nav">
<div className="nav-item" onClick={() => navigate('/home')}>Home</div>
</div>
ARIA Attributes¶
// Modal with proper ARIA
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Confirm Action</h2>
<p id="dialog-description">Are you sure you want to proceed?</p>
<button onClick={confirm}>Confirm</button>
<button onClick={cancel}>Cancel</button>
</div>
Keyboard Navigation¶
function CustomButton({ onClick, children }) {
return (
<button
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
onClick();
}
}}
tabIndex={0}
>
{children}
</button>
);
}
Documentation¶
Component Documentation¶
/**
* UserCard - Displays user information in a card format
*
* @param user - User object containing name, email, and avatar
* @param onEdit - Optional callback when edit button is clicked
* @param variant - Card style variant (default: 'default')
*
* @example
* ```tsx
* <UserCard
* user={{ name: 'John', email: 'john@example.com' }}
* onEdit={(user) => console.log('Edit', user)}
* variant="compact"
* />
* ```
*/
export function UserCard({ user, onEdit, variant = 'default' }: UserCardProps) {
// ...
}
README Template¶
# Project Name
Brief description of the project.
## Features
- Feature 1
- Feature 2
- Feature 3
## Tech Stack
- Next.js 15 (App Router)
- TypeScript
- Tailwind CSS
- Prisma
- NextAuth.js
## Getting Started
\`\`\`bash
# Install dependencies
npm install
# Set up environment variables
cp .env.example .env.local
# Run development server
npm run dev
\`\`\`
## Project Structure
\`\`\`
src/
├── app/ # Next.js App Router
├── components/ # Reusable components
├── lib/ # Utility functions
└── types/ # TypeScript types
\`\`\`
## Scripts
- `npm run dev` - Start development server
- `npm run build` - Build for production
- `npm run test` - Run tests
- `npm run lint` - Run linting
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md)
## License
MIT
Environment Setup¶
Required Files¶
.env.local:
# Database
DATABASE_URL="postgresql://..."
# Auth
NEXTAUTH_SECRET="your-secret-here"
NEXTAUTH_URL="http://localhost:3000"
# API Keys
API_KEY="your-api-key"
.env.example:
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
# Auth
NEXTAUTH_SECRET="generate-with: openssl rand -base64 32"
NEXTAUTH_URL="http://localhost:3000"
# API Keys
API_KEY="your-api-key"
.gitignore:
# Dependencies
node_modules/
.pnp/
# Testing
coverage/
# Next.js
.next/
out/
# Environment
.env*.local
# Debug
npm-debug.log*
yarn-debug.log*
# IDE
.vscode/
.idea/
Deployment Checklist¶
- Environment variables configured
- Database migrations run
- TypeScript builds without errors
- All tests passing
- No console.log in production code
- Error boundaries in place
- Loading states implemented
- SEO meta tags configured
- Analytics integrated
- Performance metrics monitored
- Security headers configured
- HTTPS enabled
- Lighthouse score > 90