コンテンツにスキップ

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:

{
  "semi": true,
  "trailingComma": "es5",
  "singleQuote": true,
  "tabWidth": 2,
  "printWidth": 100
}

Git Hooks with Husky

npm install -D husky lint-staged
npx husky init

.husky/pre-commit:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged

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

Resources