React / Next.js コーディング規約テンプレート

# React / Next.js コーディング規約

## 目次

1. [プロジェクト構成](#プロジェクト構成)
2. [コンポーネント設計](#コンポーネント設計)
3. [スタイリング](#スタイリング)
4. [状態管理](#状態管理)
5. [パフォーマンス最適化](#パフォーマンス最適化)
6. [型定義](#型定義)
7. [命名規則](#命名規則)
8. [コードフォーマット](#コードフォーマット)
9. [テスト](#テスト)
10. [アクセシビリティ](#アクセシビリティ)
11. [ドキュメント](#ドキュメント)
12. [セキュリティ](#セキュリティ)

## プロジェクト構成

### ディレクトリ構造

```
project-root/
├── .github/               # GitHub Actions 設定
├── .husky/                # Husky 設定（Git フック）
├── public/                # 静的アセット
├── src/                   # ソースコード
│   ├── app/               # App Router ルート (Next.js 13+)
│   ├── pages/             # Pages Router ルート
│   ├── components/        # コンポーネント
│   │   ├── common/        # 共通コンポーネント
│   │   ├── layout/        # レイアウト関連コンポーネント
│   │   └── features/      # 機能単位のコンポーネント
│   ├── hooks/             # カスタムフック
│   ├── lib/               # ユーティリティ関数
│   ├── services/          # API関連のサービス
│   ├── styles/            # グローバルスタイル
│   ├── types/             # 型定義
│   ├── constants/         # 定数
│   └── config/            # 設定ファイル
├── .eslintrc.js           # ESLint 設定
├── .prettierrc            # Prettier 設定
├── jest.config.js         # Jest 設定
├── next.config.js         # Next.js 設定
├── package.json           # 依存関係
└── tsconfig.json          # TypeScript 設定
```

### 環境変数

- `.env.local`、`.env.development`、`.env.production` などのファイルで環境変数を管理する
- 環境変数は`NEXT_PUBLIC_`プレフィックスを使用してクライアントサイドで利用可能な変数を明示する
- 秘密情報は必ず `.env.local` に保存し、`.gitignore` に追加する

```bash
# サーバーサイドのみで利用
API_SECRET_KEY=xxxx

# クライアントサイドでも利用可能
NEXT_PUBLIC_API_URL=https://api.example.com
```

## コンポーネント設計

### コンポーネントの分類

- **Atoms**: ボタン、入力フィールド、アイコンなどの最小単位のコンポーネント
- **Molecules**: 複数のAtomを組み合わせた小さな機能単位（例：検索バー、ナビゲーションリンク）
- **Organisms**: 複数のMoleculeを組み合わせた大きな機能単位（例：ヘッダー、フッター）
- **Templates**: ページのレイアウト構造を定義
- **Pages**: 実際のページコンテンツ

### コンポーネントの基本ルール

- 一つのコンポーネントは一つの責務のみを持つ
- プロップスの型定義を必ず行う
- 条件付きレンダリングはシンプルに保つ
- コンポーネントは小さく保ち、300行を超える場合は分割を検討する
- 大きすぎる関数も分割する（50行以上は検討対象）

### ファイル構成

#### 機能単位のディレクトリ構成

```
components/features/ProductList/
├── index.ts               # エクスポート用
├── ProductList.tsx        # メインコンポーネント
├── ProductItem.tsx        # サブコンポーネント
├── ProductFilter.tsx      # サブコンポーネント
├── ProductList.module.css # スタイル
└── ProductList.test.tsx   # テスト
```

#### コンポーネントの基本構造

```tsx
// Button.tsx
import { ReactNode } from 'react';
import styles from './Button.module.css';

type ButtonProps = {
  children: ReactNode;
  variant?: 'primary' | 'secondary';
  size?: 'small' | 'medium' | 'large';
  onClick?: () => void;
  disabled?: boolean;
};

export const Button = ({
  children,
  variant = 'primary',
  size = 'medium',
  onClick,
  disabled = false,
}: ButtonProps) => {
  return (
    <button
      className={`${styles.button} ${styles[variant]} ${styles[size]}`}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
};
```

### コンポーネントをエクスポートする方法

- index.ts ファイルを使用してコンポーネントをエクスポートする
- default export よりも named export を優先する

```tsx
// components/Button/index.ts
export * from './Button';
```

## スタイリング

### スタイリング手法

次のいずれかの方法を採用する（プロジェクト内では一貫性を保つ）：

1. **CSS Modules**: コンポーネントスコープのスタイリング
2. **Tailwind CSS**: ユーティリティファーストのスタイリング
3. **Styled Components / Emotion**: CSS-in-JS
4. **SASS / SCSS**: 拡張CSS

### CSS Modulesを使用する場合

```tsx
// Button.module.css
.button {
  padding: 0.5rem 1rem;
  border-radius: 0.25rem;
}

.primary {
  background-color: #3182ce;
  color: white;
}

// Button.tsx
import styles from './Button.module.css';

export const Button = ({ children, variant = 'primary' }) => {
  return (
    <button className={`${styles.button} ${styles[variant]}`}>
      {children}
    </button>
  );
};
```

### Tailwind CSSを使用する場合

```tsx
// Button.tsx
export const Button = ({ children, variant = 'primary' }) => {
  const variantStyles = 
    variant === 'primary' 
      ? 'bg-blue-500 text-white hover:bg-blue-600' 
      : 'bg-gray-200 text-gray-800 hover:bg-gray-300';
  
  return (
    <button className={`px-4 py-2 rounded ${variantStyles}`}>
      {children}
    </button>
  );
};
```

## 状態管理

### ローカル状態

- 単一コンポーネントのステートには `useState` または `useReducer` を使用する
- コンポーネント階層間の状態共有には Context APIを使用する

```tsx
import { useState } from 'react';

export const Counter = () => {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};
```

### グローバル状態

複雑なアプリケーションでは以下のいずれかを採用：

- **Context API + useReducer**: 中規模アプリケーション向け
- **Zustand / Jotai**: シンプルで軽量なグローバル状態
- **Redux Toolkit**: 大規模かつ複雑なアプリケーション向け

### Context APIの例

```tsx
// UserContext.tsx
import { createContext, useContext, ReactNode, useState } from 'react';

type User = {
  id: string;
  name: string;
};

type UserContextType = {
  user: User | null;
  login: (user: User) => void;
  logout: () => void;
};

const UserContext = createContext<UserContextType | undefined>(undefined);

export const UserProvider = ({ children }: { children: ReactNode }) => {
  const [user, setUser] = useState<User | null>(null);
  
  const login = (user: User) => setUser(user);
  const logout = () => setUser(null);
  
  return (
    <UserContext.Provider value={{ user, login, logout }}>
      {children}
    </UserContext.Provider>
  );
};

export const useUser = () => {
  const context = useContext(UserContext);
  if (context === undefined) {
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
};
```

## パフォーマンス最適化

### メモ化

- 不必要な再レンダリングを防ぐために `React.memo`、`useMemo`、`useCallback` を使用する
- ただし、過剰な最適化は避け、パフォーマンスの問題が確認された場合に限り適用する

```tsx
import { memo, useCallback } from 'react';

type ItemProps = {
  name: string;
  onDelete: (name: string) => void;
};

export const Item = memo(({ name, onDelete }: ItemProps) => {
  console.log(`Rendering ${name}`);
  
  return (
    <div>
      <span>{name}</span>
      <button onClick={() => onDelete(name)}>Delete</button>
    </div>
  );
});

// 親コンポーネント
export const ItemList = () => {
  const items = ['Item 1', 'Item 2', 'Item 3'];
  
  const handleDelete = useCallback((name: string) => {
    console.log(`Delete ${name}`);
  }, []);
  
  return (
    <div>
      {items.map(item => (
        <Item key={item} name={item} onDelete={handleDelete} />
      ))}
    </div>
  );
};
```

### 画像最適化

Next.jsの `Image` コンポーネントを使用して画像を最適化する：

```tsx
import Image from 'next/image';

export const ProductImage = () => {
  return (
    <div className="relative h-64 w-full">
      <Image
        src="/products/product-1.jpg"
        alt="Product"
        fill
        sizes="(max-width: 768px) 100vw, 50vw"
        priority={false}
        className="object-cover"
      />
    </div>
  );
};
```

### コード分割

- `next/dynamic` または `React.lazy` を使用してコンポーネントを動的にインポートする
- あまり使用されないコンポーネントやページ固有のコンポーネントを対象とする

```tsx
// Next.js
import dynamic from 'next/dynamic';

const DynamicChart = dynamic(() => import('../components/Chart'), {
  loading: () => <p>Loading chart...</p>,
  ssr: false, // クライアントサイドでのみレンダリング
});

// React
import { lazy, Suspense } from 'react';

const LazyChart = lazy(() => import('../components/Chart'));

export const Dashboard = () => {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<p>Loading chart...</p>}>
        <LazyChart />
      </Suspense>
    </div>
  );
};
```

## 型定義

### 基本的な型定義のガイドライン

- `any` 型の使用を避け、具体的な型を定義する
- 複数のコンポーネントで共有される型は、専用の型定義ファイルに分離する
- インターフェースの命名には接頭辞 `I` を付けない（例：`UserProps` は良いが、`IUserProps` は避ける）

```tsx
// types/user.ts
export type User = {
  id: string;
  name: string;
  email: string;
  roles: string[];
};

// types/api.ts
export type ApiResponse<T> = {
  data: T;
  status: number;
  message: string;
};

export type ApiError = {
  status: number;
  message: string;
  details?: Record<string, string[]>;
};
```

### プロップスの型定義

```tsx
import { ButtonHTMLAttributes } from 'react';

// HTMLタグの属性を継承する場合
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
  variant?: 'primary' | 'secondary';
  size?: 'small' | 'medium' | 'large';
  isLoading?: boolean;
};

export const Button = ({
  variant = 'primary',
  size = 'medium',
  isLoading = false,
  children,
  ...props
}: ButtonProps) => {
  // 実装...
};
```

## 命名規則

### ファイル命名

- **コンポーネント**: PascalCase（例：`Button.tsx`、`UserProfile.tsx`）
- **フック**: camelCaseで `use` プレフィックス（例：`useAuth.ts`、`useWindowSize.ts`）
- **ユーティリティ**: camelCase（例：`formatDate.ts`、`stringUtils.ts`）
- **CSS Modules**: コンポーネントと同名（例：`Button.module.css`）

### 変数とプロパティの命名

- **一般変数**: camelCase（例：`userData`、`isLoading`）
- **定数**: UPPER_SNAKE_CASE（例：`MAX_RETRY_COUNT`、`API_BASE_URL`）
- **真偽値**: `is`、`has`、`should` などのプレフィックス（例：`isActive`、`hasPermission`）
- **イベントハンドラ**: `handle` または `on` プレフィックス（例：`handleSubmit`、`onUserChange`）

### コンポーネントとフックの命名

```tsx
// コンポーネント
export const UserProfileCard = () => { /* ... */ };

// イベントハンドラ
const handleSubmit = () => { /* ... */ };

// カスタムフック
export const useUserProfile = () => {
  // ...
  return { user, loading, error, updateUser };
};
```

## コードフォーマット

### ESLintとPrettier

以下のツールを設定して使用する：

- **ESLint**: コード品質の検証
- **Prettier**: コードフォーマット
- **Husky**: コミット前に自動でフォーマットやlintチェックを実行

### .eslintrc.js の例

```js
module.exports = {
  extends: [
    'next/core-web-vitals',
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:jsx-a11y/recommended',
    'prettier',
  ],
  rules: {
    'react/react-in-jsx-scope': 'off',
    'react/prop-types': 'off',
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
    'jsx-a11y/anchor-is-valid': 'warn',
  },
};
```

### .prettierrc の例

```json
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "bracketSpacing": true,
  "arrowParens": "avoid"
}
```

## テスト

### テストのアプローチ

- **ユニットテスト**: 個々のコンポーネントや関数のテスト
- **インテグレーションテスト**: 複数のコンポーネントの相互作用のテスト
- **E2Eテスト**: ユーザーの視点でのアプリケーション全体のテスト

### テストのフレームワークとライブラリ

- **Jest**: テストランナー
- **React Testing Library**: コンポーネントのテスト
- **Cypress / Playwright**: E2Eテスト

### コンポーネントテストの例

```tsx
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

describe('Button', () => {
  it('renders with correct text', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('calls onClick when clicked', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click me</Button>);
    fireEvent.click(screen.getByText('Click me'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('is disabled when disabled prop is true', () => {
    render(<Button disabled>Click me</Button>);
    expect(screen.getByText('Click me')).toBeDisabled();
  });
});
```

## アクセシビリティ

### 基本ルール

- セマンティックなHTML要素を使用する
- インタラクティブ要素には適切なARIAロールを設定する
- フォーム要素には適切なラベルを付ける
- キーボードナビゲーションをサポートする
- 色のコントラスト比を確保する（WCAG AA基準以上）

### アクセシブルなコンポーネントの例

```tsx
// アクセシブルなモーダル
import { useEffect, useRef } from 'react';

type ModalProps = {
  isOpen: boolean;
  onClose: () => void;
  title: string;
  children: React.ReactNode;
};

export const Modal = ({ isOpen, onClose, title, children }: ModalProps) => {
  const modalRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
    };

    if (isOpen) {
      document.addEventListener('keydown', handleEscape);
      // フォーカストラップの実装
      modalRef.current?.focus();
    }

    return () => {
      document.removeEventListener('keydown', handleEscape);
    };
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return (
    <div
      className="modal-overlay"
      onClick={onClose}
      role="presentation"
    >
      <div
        ref={modalRef}
        className="modal"
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        tabIndex={-1}
        onClick={e => e.stopPropagation()}
      >
        <header>
          <h2 id="modal-title">{title}</h2>
          <button
            onClick={onClose}
            aria-label="Close modal"
            className="close-button"
          >
            ×
          </button>
        </header>
        <div className="modal-content">{children}</div>
      </div>
    </div>
  );
};
```

## ドキュメント

### コンポーネントドキュメント

- **Storybook**を使用してコンポーネントの使用例を示す
- JSDocスタイルのコメントでコンポーネントとプロップスを説明する

```tsx
/**
 * プライマリボタンコンポーネント
 * ユーザーアクションのトリガーに使用します
 *
 * @example
 * ```tsx
 * <Button variant="primary" onClick={handleClick}>
 *   送信する
 * </Button>
 * ```
 */
export const Button = ({
  variant = 'primary',
  size = 'medium',
  children,
  ...props
}: ButtonProps) => {
  // 実装...
};
```

### プロジェクトドキュメント

README.mdファイルに以下の情報を含める：

- プロジェクトの概要と目的
- 開発環境のセットアップ手順
- 利用可能なスクリプトとコマンド
- アーキテクチャの概要
- コントリビューションのガイドライン

## セキュリティ

### セキュリティのベストプラクティス

- ユーザー入力は常にサニタイズする
- クロスサイトスクリプティング（XSS）を防止する
- 機密情報はクライアント側には保存しない
- APIルートでは適切な検証とエラーハンドリングを行う
- 最新の依存関係を維持する

```tsx
// 安全なデータレンダリング
import DOMPurify from 'dompurify';

export const SafeHTML = ({ html }: { html: string }) => {
  const sanitizedHTML = DOMPurify.sanitize(html);
  
  return <div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />;
};

// APIエンドポイントでのバリデーション
// pages/api/users.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { z } from 'zod';

const UserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  age: z.number().min(18).optional(),
});

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') {
    return res.status(405).json({ message: 'Method not allowed' });
  }
  
  try {
    const validatedData = UserSchema.parse(req.body);
    // データ処理...
    return res.status(201).json({ success: true });
  } catch (error) {
    return res.status(400).json({ message: 'Invalid input', error });
  }
}
```

---