T-CREATOR

React 開発を加速する GitHub Copilot 活用レシピ 20 選

React 開発を加速する GitHub Copilot 活用レシピ 20 選

React 開発の現場では、「もっと効率的にコードを書きたい」「品質の高いコンポーネントを素早く作成したい」という声をよく耳にします。そんな開発者の皆さまに朗報です。GitHub Copilot を活用することで、React 開発の生産性を飛躍的に向上させることができるんです。

本記事では、React 開発における GitHub Copilot の具体的な活用方法を、20 の実践的なレシピとしてご紹介いたします。基礎的なコンポーネント作成から、複雑な状態管理、テスト自動生成まで、段階的に学べる構成となっております。ぜひ最後までお読みいただき、明日からの開発に活かしてくださいね。

この記事の読み方と活用方法

記事の構成と学習の進め方

この記事は、GitHub Copilot を使った React 開発の効率化を段階的に学習できるよう設計されています。以下の流れで読み進めることをお勧めいたします。

1. 基礎理解(背景・課題・解決策) まずは React 開発の現状と GitHub Copilot の基本概念を理解しましょう。なぜ AI 支援開発が重要なのか、従来の開発手法の課題と解決策を把握することで、後続のレシピの価値がより明確になります。

2. 段階的なレシピ学習

  • 基礎編(レシピ1-7): React 初心者の方は必ずここから始めてください
  • 中級編(レシピ8-14): 基礎編をマスターした方、または1年以上の React 経験者向け
  • 応用編(レシピ15-20): チームリードやエンタープライズ開発者向け

3. 実践とベストプラクティス まとめ章では、学んだレシピを統合的に活用する方法や、チーム導入のポイントを解説しています。

レシピの実践方法

各レシピは以下の構成になっています:

レシピタイトル
├── 実装シーン(いつ使うか)
├── GitHub Copilot への指示方法
├── 生成されるコード例
├── カスタマイズのポイント
└── よくある失敗例と対処法

実際の開発での活用手順

  1. シーン識別: 「今の作業はどのレシピに該当するか」を判断
  2. コメント作成: レシピを参考に、明確で具体的なコメントを記述
  3. Copilot 活用: コメント後に Tab キーで提案を受け取り
  4. コード調整: 提案されたコードをプロジェクトに合わせてカスタマイズ
  5. 検証: 動作確認とテストの実行

レシピごとの使用シーン

以下は各レシピがどのような開発場面で役立つかの早見表です:

レシピ番号使用シーン効果
1-3新規コンポーネント作成時初期実装時間を70%短縮
4-5TypeScript での型安全性確保型エラーを80%削減
6-7UI ロジックの実装条件分岐・リスト表示の高速化
8-10中規模アプリの状態管理再利用可能なロジック作成
11-12API 通信・ルーティングネットワーク処理の標準化
13-14プロダクション品質確保エラーハンドリング・最適化
15-17品質保証・開発ツールテスト・ドキュメント自動化
18-20エンタープライズ開発SSR・型安全性・デバッグ

効果的な学習のコツ

手を動かしながら学習する コード例をそのまま写すのではなく、実際に GitHub Copilot を使って同様のコードを生成してみてください。AI の提案パターンを体感することが重要です。

プロジェクトに合わせてカスタマイズ レシピのコードは汎用的な例です。実際のプロジェクトでは、命名規則、設計パターン、使用ライブラリに合わせて調整してください。

失敗を恐れずに試行錯誤 GitHub Copilot は学習するツールです。様々なコメントパターンを試し、どのような指示が良い結果を生むかを実験してみましょう。

よくある質問と解答

Q: GitHub Copilot の料金はかかりますか? A: 個人利用は月額$10、企業利用は月額$19です。学生や OSS コントリビューターは無料で利用できます。

Q: レシピの順番通りに学習する必要がありますか? A: 基礎編は順番に学習することをお勧めしますが、中級編・応用編は必要に応じて選択的に学習していただけます。

Q: 既存プロジェクトにも適用できますか? A: はい。レシピは新規開発だけでなく、既存コードのリファクタリングや機能追加にも活用できます。

Q: チーム導入時の注意点は? A: まとめ章の「チーム開発での活用指針」を参考に、共通ルールの策定と段階的な導入をお勧めします。

背景

React 開発の現状と課題

現代の Web 開発において、React は圧倒的な人気を誇るフロントエンドライブラリです。しかし、その人気の裏で開発者が直面する課題も多岐にわたります。

React エコシステムの急速な進化により、新しいパターンやベストプラクティスが次々と登場しています。関数コンポーネント、Hooks、TypeScript との組み合わせなど、学習すべき要素が増え続けているのが現状です。

また、プロジェクトの規模が大きくなるにつれて、コンポーネントの設計、状態管理、パフォーマンス最適化といった複雑な課題に直面することが多くなりました。

以下の図は、React 開発における典型的な課題の関係性を示しています。

mermaidflowchart TD
    A[React プロジェクト] --> B[学習コスト]
    A --> C[開発効率]
    A --> D[コード品質]
    B --> E[新技術の習得]
    B --> F[ベストプラクティス]
    C --> G[繰り返し作業]
    C --> H[デバッグ時間]
    D --> I[一貫性の確保]
    D --> J[メンテナンス性]
    E --> K[生産性低下]
    F --> K
    G --> K
    H --> K
    I --> L[開発チーム課題]
    J --> L

この図からわかるように、各課題は相互に関連し合い、最終的に開発チーム全体の生産性に影響を与えています。

GitHub Copilot の可能性

GitHub Copilot は、OpenAI の技術をベースとした AI ペアプログラミングツールです。数十億行のコードで学習されており、コンテキストを理解して適切なコード提案を行うことができます。

React 開発においては、以下のような強力な支援を提供してくれます。

コード生成の高速化 コメントや関数名から、完全な実装を自動生成できます。useState や useEffect といった Hooks の実装パターンも瞬時に提案されるでしょう。

ベストプラクティスの学習支援 AI が学習した大量のコードベースから、現在のプロジェクトに適したパターンを提案してくれます。これにより、自然とベストプラクティスを身につけることができるんです。

TypeScript との相性の良さ 型定義や型安全なコードの生成において、特に優れた能力を発揮します。プロップスの型定義やイベントハンドラーの実装が驚くほどスムーズになります。

AI 支援開発の重要性

現代のソフトウェア開発において、AI 支援ツールの活用は単なる「便利機能」ではなく、競争力維持のための「必須スキル」となりつつあります。

GitHub Copilot を活用することで、開発者は以下のメリットを得ることができます。

mermaidflowchart LR
    A[AI 支援開発] --> B[創造的作業への集中]
    A --> C[学習効率の向上]
    A --> D[品質の標準化]
    B --> E[より良い設計]
    B --> F[ユーザー体験の向上]
    C --> G[新技術習得の加速]
    C --> H[チーム全体のスキルアップ]
    D --> I[保守性の向上]
    D --> J[バグの削減]

繰り返し作業を AI に任せることで、開発者はより高次の問題解決や創造的な作業に時間を割くことができるようになります。これこそが、AI 支援開発の最大の価値と言えるでしょう。

図で理解できる要点:

  • React 開発の課題は相互に関連し合っている
  • AI 支援により、創造的作業と学習効率が同時に向上する
  • 品質の標準化が開発チーム全体のメリットにつながる

課題

従来の React 開発で直面する問題

React 開発において、多くの開発者が共通して抱える課題があります。これらの問題を具体的に見ていきましょう。

繰り返し作業による時間の浪費 新しいコンポーネントを作成するたびに、同じようなボイラープレートコードを書く必要があります。useState の初期化、useEffect の設定、プロップスの型定義など、似たようなパターンを何度も実装することになるのです。

ベストプラクティスの把握困難 React エコシステムは常に進化しており、「正しい」実装方法を把握するのが困難になっています。クラスコンポーネントから関数コンポーネントへの移行、Redux から Context API への変更など、パラダイムシフトが頻繁に起こります。

TypeScript との統合における複雑さ 型安全な React アプリケーションを構築するためには、適切な型定義が不可欠です。しかし、プロップスの型定義、イベントハンドラーの型、ジェネリクスの活用など、学習コストが高い要素が多数存在します。

コード品質と開発効率のジレンマ

開発現場では、「速く作る」ことと「良いコードを書く」ことの間で常にバランスを取る必要があります。

mermaidflowchart TD
    A[開発要求] --> B[スピード重視]
    A --> C[品質重視]
    B --> D[技術債務の蓄積]
    B --> E[後からのリファクタリング]
    C --> F[開発速度の低下]
    C --> G[リリースの遅延]
    D --> H[メンテナンス困難]
    E --> I[追加開発コスト]
    F --> J[競争力低下]
    G --> J
    H --> K[長期的な問題]
    I --> K
    J --> K

この図が示すように、どちらを優先しても長期的な問題につながる可能性があります。理想的には、スピードと品質を両立できる開発手法が求められているのです。

急ぎの実装による技術債務 リリース期限に追われて、とりあえず動くコードを書いてしまうケースが多々あります。その結果、後からのメンテナンスが困難になり、新機能追加の際に大幅な修正が必要になることも少なくありません。

レビュープロセスの負荷 コード品質を維持するためには、徹底的なコードレビューが必要です。しかし、レビューに時間をかけすぎると開発速度が低下し、プロジェクト全体のスケジュールに影響を与えてしまいます。

学習コストと生産性のバランス

React エコシステムの学習は、継続的な投資が必要な分野です。新しい技術やパターンを学習する時間と、実際の開発作業に充てる時間のバランスを取ることが重要になります。

新技術習得の時間確保 React Hooks、Suspense、Concurrent Features など、新しい機能が次々とリリースされています。これらを適切に学習するための時間を確保しつつ、日常の開発業務を遂行する必要があります。

チーム内でのスキル格差 経験豊富な開発者と初心者の間で、React に関する知識やスキルに大きな差が生まれがちです。この格差が開発効率や品質に影響を与えることも多いでしょう。

実践的な学習機会の不足 書籍や動画で学んだ知識を、実際のプロジェクトで活用する機会が限られていることがあります。理論と実践のギャップを埋めるのに時間がかかってしまうのです。

図で理解できる要点:

  • 開発要求における速度と品質のトレードオフが長期的な問題を生む
  • 学習投資と開発生産性のバランスが重要
  • チーム全体のスキル向上が継続的な課題となる

解決策

GitHub Copilot を活用した開発手法

これまでに挙げた課題を解決するため、GitHub Copilot を活用した効率的な React 開発手法をご提案いたします。

GitHub Copilot は、単なるコード補完ツールではありません。開発者の意図を理解し、適切なパターンを提案する「AI ペアプログラマー」として機能します。以下の図は、GitHub Copilot を活用した開発フローを示しています。

mermaidflowchart TD
    A[開発要求] --> B[コメントでの意図表現]
    B --> C[GitHub Copilot による提案]
    C --> D[コード生成・修正]
    D --> E[即座のフィードバック]
    E --> F[品質向上]
    F --> G[学習効果]
    G --> H[次回の開発効率向上]
    H --> A

    C --> I[複数の実装パターン提示]
    I --> J[ベストプラクティスの学習]
    J --> G

    D --> K[TypeScript 型安全性]
    K --> L[バグの早期発見]
    L --> F

この循環的なプロセスにより、開発効率と品質向上を同時に実現できるのです。

即座のコード生成による時間短縮 コメントや関数名を書くだけで、完全な実装が提案されます。useState、useEffect、カスタムフックなど、React の一般的なパターンはほぼ瞬時に生成可能です。

これまで 10 分かかっていたボイラープレートコードの作成が、数秒で完了するようになります。削減された時間は、より重要な設計やユーザー体験の改善に充てることができるでしょう。

ベストプラクティスの自然な習得 GitHub Copilot は、数十億行のコードから学習しているため、現在のベストプラクティスに沿った実装を提案してくれます。

例えば、useState の使い方、useEffect の依存配列の設定、メモ化の適用タイミングなど、経験豊富な開発者が実践しているパターンを自然と学ぶことができます。

型安全性の向上 TypeScript との組み合わせにおいて、Copilot は特に威力を発揮します。プロップスの型定義、イベントハンドラーの型、ジェネリクスの活用など、型安全なコードの作成を強力に支援します。

型エラーの発生頻度が大幅に減り、実行時エラーの予防につながるのです。

学習効果の最大化 提案されたコードを読み解くことで、新しいパターンや手法を学習できます。これは、従来の書籍や動画学習とは異なり、実際のコンテキストの中で学習できる点が大きなメリットです。

チーム全体のスキル向上 GitHub Copilot を導入することで、チーム内のスキル格差を自然と縮小できます。初心者は熟練者のパターンを学び、熟練者は新しい手法を発見する機会が増えるでしょう。

以下の表は、従来の開発手法と GitHub Copilot を活用した開発手法の比較です。

項目従来の手法GitHub Copilot 活用改善効果
コード作成時間手作業で実装AI 支援による高速生成70-80% 短縮
ベストプラクティス習得個人学習に依存提案から自然に学習継続的な向上
型安全性確保経験とレビューに依存AI による適切な型推論エラー率 50% 削減
チームスキル格差個人の経験に左右共通の AI 支援格差の縮小
学習効率時間を確保して学習開発しながら学習実践的なスキル習得

この表からもわかるように、GitHub Copilot の活用により、従来の課題を効果的に解決できることがお分かりいただけるでしょう。

図で理解できる要点:

  • GitHub Copilot による開発フローは循環的で継続的な改善を生む
  • 時間短縮と品質向上を同時に実現
  • チーム全体のスキル向上に貢献する仕組み

基礎編(レシピ 1-7)

ここからは、GitHub Copilot を活用した具体的なレシピを段階的にご紹介していきます。基礎編では、React 開発の土台となる基本的なパターンを効率的に実装する方法を学んでいただきます。

レシピ 1: コンポーネントの雛形生成

いつ使うか

  • 新しい機能を実装する際に、最初のコンポーネントファイルを作成するとき
  • 既存のコンポーネントと似た構造のコンポーネントが必要なとき
  • プロトタイプを素早く作成したいとき

新しい React コンポーネントを作成する際、GitHub Copilot を活用すると驚くほど簡単に雛形を生成できます。手動でボイラープレートを書く時間を大幅に短縮できるでしょう。

Copilot への指示方法

まず、ファイルの先頭にコメントで作りたいコンポーネントの概要を書きます。

typescript// ユーザー情報を表示するカードコンポーネント
// プロップス: name, email, avatar
// クリック時にユーザー詳細ページに遷移

このコメントを書いた瞬間、Copilot が以下のような完全なコンポーネントを提案してくれます。

typescriptimport React from 'react';

interface UserCardProps {
  name: string;
  email: string;
  avatar: string;
  onClick?: () => void;
}

export const UserCard: React.FC<UserCardProps> = ({
  name,
  email,
  avatar,
  onClick,
}) => {
  return (
    <div className='user-card' onClick={onClick}>
      <img src={avatar} alt={name} className='avatar' />
      <div className='user-info'>
        <h3>{name}</h3>
        <p>{email}</p>
      </div>
    </div>
  );
};

活用のポイント

  1. 明確な意図の表現: コメントで「何を」「どのように」作りたいかを具体的に記述することで、より適切な提案を得られます
  2. 型定義の自動生成: TypeScript を使用している場合、プロップスの型定義も自動で生成されます
  3. イベントハンドラーの考慮: クリック処理など、一般的な機能も自動で考慮されます

よくある失敗例と対処法

  • ❌ 「コンポーネント作って」→ 曖昧すぎて適切な提案が得られない
  • ✅ 「ユーザー情報表示用のカードコンポーネント、name・email・avatarをプロップスとして受け取る」→ 具体的で明確

レシピ 2: useState フックの実装

いつ使うか

  • フォームの入力値を管理したいとき
  • UI の表示状態(モーダルの開閉、ローディング状態など)を制御したいとき
  • カウンターや切り替えボタンなどのシンプルな状態が必要なとき

状態管理は React 開発の基本中の基本です。GitHub Copilot を使えば、適切な useState の実装パターンを瞬時に生成できます。型安全性も自動で確保されるため、TypeScript プロジェクトでは特に威力を発揮します。

基本的な状態管理

typescript// カウンターの状態管理
const [count, setCount] = useState<number>(0);

// カウントアップ関数
const incrementCount = () => {
  setCount((prev) => prev + 1);
};

複雑なオブジェクト状態の管理

コメントで意図を示すと、複雑な状態管理も適切に実装してくれます。

typescript// ユーザーフォームの状態管理
interface UserForm {
  name: string;
  email: string;
  isValid: boolean;
}

const [userForm, setUserForm] = useState<UserForm>({
  name: '',
  email: '',
  isValid: false,
});

// フォーム更新関数
const updateUserForm = (
  field: keyof UserForm,
  value: string | boolean
) => {
  setUserForm((prev) => ({
    ...prev,
    [field]: value,
    isValid:
      prev.name.length > 0 && prev.email.includes('@'),
  }));
};

配列状態の操作

typescript// TODO リストの状態管理
interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

const [todos, setTodos] = useState<Todo[]>([]);

// TODO 追加
const addTodo = (text: string) => {
  const newTodo: Todo = {
    id: Date.now(),
    text,
    completed: false,
  };
  setTodos((prev) => [...prev, newTodo]);
};

// TODO 完了切り替え
const toggleTodo = (id: number) => {
  setTodos((prev) =>
    prev.map((todo) =>
      todo.id === id
        ? { ...todo, completed: !todo.completed }
        : todo
    )
  );
};

レシピ 3: useEffect フックの実装

副作用の処理は React アプリケーションにおいて重要な要素です。GitHub Copilot は、useEffect の適切な使用パターンを提案してくれます。

API データの取得

typescript// ユーザーデータを取得する useEffect
useEffect(() => {
  const fetchUserData = async () => {
    try {
      const response = await fetch('/api/user');
      const userData = await response.json();
      setUser(userData);
    } catch (error) {
      console.error('Failed to fetch user data:', error);
      setError('ユーザー情報の取得に失敗しました');
    } finally {
      setLoading(false);
    }
  };

  fetchUserData();
}, []);

リアルタイム更新の実装

typescript// WebSocket を使ったリアルタイム通信
useEffect(() => {
  const socket = new WebSocket('ws://localhost:8080');

  socket.onmessage = (event) => {
    const message = JSON.parse(event.data);
    setMessages((prev) => [...prev, message]);
  };

  socket.onerror = (error) => {
    console.error('WebSocket error:', error);
  };

  // クリーンアップ関数
  return () => {
    socket.close();
  };
}, []);

依存配列を持つ useEffect

typescript// 検索クエリが変更されたときの処理
useEffect(() => {
  const debounceTimeout = setTimeout(() => {
    if (searchQuery.trim()) {
      searchUsers(searchQuery);
    }
  }, 300);

  return () => {
    clearTimeout(debounceTimeout);
  };
}, [searchQuery]);

レシピ 4: プロップスの型定義

TypeScript を使用した React 開発において、プロップスの型定義は重要な要素です。GitHub Copilot は、適切な型定義を自動生成してくれます。

基本的なプロップス型定義

typescript// ボタンコンポーネントのプロップス
interface ButtonProps {
  children: React.ReactNode;
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
  onClick?: () => void;
}

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

ジェネリクスを使った柔軟な型定義

typescript// リストコンポーネントのジェネリック型定義
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
  emptyMessage?: string;
}

export function List<T>({
  items,
  renderItem,
  keyExtractor,
  emptyMessage = 'アイテムがありません',
}: ListProps<T>) {
  if (items.length === 0) {
    return (
      <div className='empty-message'>{emptyMessage}</div>
    );
  }

  return (
    <ul className='list'>
      {items.map((item, index) => (
        <li key={keyExtractor(item)}>
          {renderItem(item, index)}
        </li>
      ))}
    </ul>
  );
}

レシピ 5: イベントハンドラーの作成

React アプリケーションにおけるイベント処理は、ユーザーとのインタラクションを実現する重要な要素です。

フォーム送信ハンドラー

typescript// フォーム送信の処理
const handleSubmit = async (
  event: React.FormEvent<HTMLFormElement>
) => {
  event.preventDefault();
  setLoading(true);

  try {
    const formData = new FormData(event.currentTarget);
    const data = Object.fromEntries(formData.entries());

    const response = await fetch('/api/contact', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(data),
    });

    if (response.ok) {
      setSuccess(true);
      event.currentTarget.reset();
    } else {
      throw new Error('送信に失敗しました');
    }
  } catch (error) {
    setError(
      error instanceof Error
        ? error.message
        : '予期しないエラーが発生しました'
    );
  } finally {
    setLoading(false);
  }
};

入力値変更ハンドラー

typescript// 入力フィールドの変更処理
const handleInputChange = (
  event: React.ChangeEvent<HTMLInputElement>
) => {
  const { name, value, type, checked } = event.target;

  setFormData((prev) => ({
    ...prev,
    [name]: type === 'checkbox' ? checked : value,
  }));

  // バリデーション処理
  validateField(
    name,
    type === 'checkbox' ? checked : value
  );
};

キーボードイベントハンドラー

typescript// Enterキーでの検索実行
const handleKeyPress = (
  event: React.KeyboardEvent<HTMLInputElement>
) => {
  if (event.key === 'Enter' && !event.shiftKey) {
    event.preventDefault();
    handleSearch();
  }
};

レシピ 6: 条件分岐レンダリング

React における条件分岐レンダリングは、動的な UI を実現するために重要なテクニックです。

基本的な条件分岐

typescript// ローディング状態とエラー状態の表示
const UserProfile: React.FC<{ userId: string }> = ({
  userId,
}) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  if (loading) {
    return (
      <LoadingSpinner message='ユーザー情報を読み込み中...' />
    );
  }

  if (error) {
    return (
      <ErrorMessage
        message={error}
        onRetry={() => fetchUser(userId)}
      />
    );
  }

  if (!user) {
    return <div>ユーザーが見つかりません</div>;
  }

  return (
    <div className='user-profile'>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
};

複雑な条件分岐

typescript// 権限による表示制御
const AdminPanel: React.FC = () => {
  const { user, permissions } = useAuth();

  return (
    <div className='admin-panel'>
      {permissions.canViewDashboard && <DashboardWidget />}

      {permissions.canManageUsers ? (
        <UserManagement />
      ) : (
        <AccessDenied message='ユーザー管理権限が必要です' />
      )}

      {user.role === 'super_admin' && <SystemSettings />}
    </div>
  );
};

レシピ 7: リスト表示の実装

配列データの表示は React アプリケーションにおいて頻繁に行う処理です。効率的なリスト表示のパターンを学びましょう。

基本的なリスト表示

typescript// 商品一覧の表示
interface Product {
  id: number;
  name: string;
  price: number;
  image: string;
}

const ProductList: React.FC<{ products: Product[] }> = ({
  products,
}) => {
  return (
    <div className='product-grid'>
      {products.map((product) => (
        <div key={product.id} className='product-card'>
          <img src={product.image} alt={product.name} />
          <h3>{product.name}</h3>
          <p className='price'>
            ¥{product.price.toLocaleString()}
          </p>
        </div>
      ))}
    </div>
  );
};

フィルタリング機能付きリスト

typescript// フィルタ機能付きの商品一覧
const FilterableProductList: React.FC = () => {
  const [products, setProducts] = useState<Product[]>([]);
  const [filter, setFilter] = useState({
    category: '',
    minPrice: 0,
    maxPrice: 100000,
    searchQuery: '',
  });

  const filteredProducts = useMemo(() => {
    return products.filter((product) => {
      const matchesCategory =
        !filter.category ||
        product.category === filter.category;
      const matchesPrice =
        product.price >= filter.minPrice &&
        product.price <= filter.maxPrice;
      const matchesSearch =
        !filter.searchQuery ||
        product.name
          .toLowerCase()
          .includes(filter.searchQuery.toLowerCase());

      return (
        matchesCategory && matchesPrice && matchesSearch
      );
    });
  }, [products, filter]);

  return (
    <div>
      <ProductFilter
        filter={filter}
        onFilterChange={setFilter}
      />
      <ProductList products={filteredProducts} />
    </div>
  );
};

無限スクロール対応リスト

typescript// 無限スクロールによるページング
const InfiniteProductList: React.FC = () => {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);

  const loadMoreProducts = useCallback(async () => {
    if (loading || !hasMore) return;

    setLoading(true);
    try {
      const response = await fetch(
        `/api/products?offset=${products.length}&limit=20`
      );
      const newProducts = await response.json();

      if (newProducts.length < 20) {
        setHasMore(false);
      }

      setProducts((prev) => [...prev, ...newProducts]);
    } catch (error) {
      console.error('Failed to load products:', error);
    } finally {
      setLoading(false);
    }
  }, [products.length, loading, hasMore]);

  // スクロール監視
  useEffect(() => {
    const handleScroll = () => {
      if (
        window.innerHeight +
          document.documentElement.scrollTop >=
        document.documentElement.offsetHeight - 1000
      ) {
        loadMoreProducts();
      }
    };

    window.addEventListener('scroll', handleScroll);
    return () =>
      window.removeEventListener('scroll', handleScroll);
  }, [loadMoreProducts]);

  return (
    <div>
      <ProductList products={products} />
      {loading && <LoadingSpinner />}
      {!hasMore && (
        <div className='end-message'>
          全ての商品を表示しました
        </div>
      )}
    </div>
  );
};

基礎編で学んだパターンは、React 開発の土台となる重要な要素です。これらのレシピを活用することで、効率的で品質の高いコンポーネント開発が可能になるでしょう。

中級編(レシピ 8-14)

中級編では、より実践的で高度な React 開発パターンを GitHub Copilot と共に実装していきます。複雑な状態管理やパフォーマンス最適化など、実際のプロダクション環境で必要となるスキルを身につけていただけます。

レシピ 8: カスタムフックの作成

いつ使うか

  • 複数のコンポーネントで同じ状態ロジックを使いまわしたいとき
  • API 通信やローカルストレージへの読み書きを共通化したいとき
  • 複雑な状態管理を別ファイルに切り出してコンポーネントをシンプルにしたいとき

カスタムフックは、ロジックの再利用とコンポーネントの関心の分離を実現する重要な手法です。GitHub Copilot を活用することで、効率的にカスタムフックを作成できます。一度作成すれば、プロジェクト全体で再利用できる強力な仕組みです。

データフェッチング用のカスタムフック

typescript// API データフェッチング用のカスタムフック
interface UseFetchResult<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
  refetch: () => void;
}

const useFetch = <T>(url: string): UseFetchResult<T> => {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const fetchData = useCallback(async () => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err instanceof Error ? err.message : '予期しないエラーが発生しました');
    } finally {
      setLoading(false);
    }
  }, [url]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return { data, loading, error, refetch: fetchData };
};

// 使用例
const UserProfile: React.FC<{ userId: string }> = ({ userId }) => {
  const { data: user, loading, error, refetch } = useFetch<User>(`/api/users/${userId}`);

  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorMessage message={error} onRetry={refetch} />;
  if (!user) return <div>ユーザーが見つかりません</div>;

  return <UserCard user={user} />;
};

ローカルストレージ連携フック

typescript// ローカルストレージと状態を同期するカスタムフック
const useLocalStorage = <T>(key: string, initialValue: T) => {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  const setValue = useCallback((value: T | ((val: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  }, [key, storedValue]);

  return [storedValue, setValue] as const;
};

// 使用例
const Settings: React.FC = () => {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [language, setLanguage] = useLocalStorage('language', 'ja');

  return (
    <div>
      <select value={theme} onChange={(e) => setTheme(e.target.value)}>
        <option value="light">ライトテーマ</option>
        <option value="dark">ダークテーマ</option>
      </select>
    </div>
  );
};

レシピ 9: Context API の実装

Context API を使用したグローバル状態管理は、中級レベルの React 開発において重要なスキルです。

認証状態管理の Context

typescript// 認証状態の型定義
interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

interface AuthContextType {
  user: User | null;
  login: (
    email: string,
    password: string
  ) => Promise<boolean>;
  logout: () => void;
  loading: boolean;
}

// Context の作成
const AuthContext = createContext<
  AuthContextType | undefined
>(undefined);

// Provider コンポーネント
export const AuthProvider: React.FC<{
  children: React.ReactNode;
}> = ({ children }) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  // 初期化時の認証状態確認
  useEffect(() => {
    const checkAuthStatus = async () => {
      try {
        const token = localStorage.getItem('authToken');
        if (token) {
          const response = await fetch('/api/auth/verify', {
            headers: { Authorization: `Bearer ${token}` },
          });

          if (response.ok) {
            const userData = await response.json();
            setUser(userData);
          }
        }
      } catch (error) {
        console.error('Auth verification failed:', error);
      } finally {
        setLoading(false);
      }
    };

    checkAuthStatus();
  }, []);

  const login = async (
    email: string,
    password: string
  ): Promise<boolean> => {
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });

      if (response.ok) {
        const { user: userData, token } =
          await response.json();
        localStorage.setItem('authToken', token);
        setUser(userData);
        return true;
      }
      return false;
    } catch (error) {
      console.error('Login failed:', error);
      return false;
    }
  };

  const logout = () => {
    localStorage.removeItem('authToken');
    setUser(null);
  };

  const value = { user, login, logout, loading };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
};

// カスタムフック
export const useAuth = () => {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error(
      'useAuth must be used within an AuthProvider'
    );
  }
  return context;
};

レシピ 10: フォームバリデーション

複雑なフォームバリデーションは、ユーザビリティとデータ品質の両方にとって重要です。

包括的なフォームバリデーション

typescript// バリデーションルールの型定義
interface ValidationRule {
  required?: boolean;
  minLength?: number;
  maxLength?: number;
  pattern?: RegExp;
  custom?: (value: any) => string | null;
}

interface ValidationRules {
  [field: string]: ValidationRule;
}

// フォームバリデーションフック
const useFormValidation = <T extends Record<string, any>>(
  initialValues: T,
  rules: ValidationRules
) => {
  const [values, setValues] = useState<T>(initialValues);
  const [errors, setErrors] = useState<
    Partial<Record<keyof T, string>>
  >({});
  const [touched, setTouched] = useState<
    Partial<Record<keyof T, boolean>>
  >({});

  const validateField = (
    name: keyof T,
    value: any
  ): string | null => {
    const rule = rules[name as string];
    if (!rule) return null;

    if (
      rule.required &&
      (!value || value.toString().trim() === '')
    ) {
      return 'この項目は必須です';
    }

    if (
      rule.minLength &&
      value.toString().length < rule.minLength
    ) {
      return `${rule.minLength}文字以上で入力してください`;
    }

    if (
      rule.maxLength &&
      value.toString().length > rule.maxLength
    ) {
      return `${rule.maxLength}文字以下で入力してください`;
    }

    if (
      rule.pattern &&
      !rule.pattern.test(value.toString())
    ) {
      return '正しい形式で入力してください';
    }

    if (rule.custom) {
      return rule.custom(value);
    }

    return null;
  };

  const handleChange = (name: keyof T, value: any) => {
    setValues((prev) => ({ ...prev, [name]: value }));

    if (touched[name]) {
      const error = validateField(name, value);
      setErrors((prev) => ({
        ...prev,
        [name]: error || undefined,
      }));
    }
  };

  const handleBlur = (name: keyof T) => {
    setTouched((prev) => ({ ...prev, [name]: true }));
    const error = validateField(name, values[name]);
    setErrors((prev) => ({
      ...prev,
      [name]: error || undefined,
    }));
  };

  const validateAll = (): boolean => {
    const newErrors: Partial<Record<keyof T, string>> = {};
    let isValid = true;

    Object.keys(rules).forEach((field) => {
      const error = validateField(
        field as keyof T,
        values[field as keyof T]
      );
      if (error) {
        newErrors[field as keyof T] = error;
        isValid = false;
      }
    });

    setErrors(newErrors);
    setTouched(
      Object.keys(rules).reduce((acc, field) => {
        acc[field as keyof T] = true;
        return acc;
      }, {} as Partial<Record<keyof T, boolean>>)
    );

    return isValid;
  };

  return {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    validateAll,
    isValid: Object.keys(errors).length === 0,
  };
};

// 使用例
interface UserFormData {
  name: string;
  email: string;
  password: string;
  confirmPassword: string;
}

const UserRegistrationForm: React.FC = () => {
  const validationRules: ValidationRules = {
    name: { required: true, minLength: 2, maxLength: 50 },
    email: {
      required: true,
      pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
    },
    password: { required: true, minLength: 8 },
    confirmPassword: {
      required: true,
      custom: (value) =>
        value !== form.values.password
          ? 'パスワードが一致しません'
          : null,
    },
  };

  const form = useFormValidation<UserFormData>(
    {
      name: '',
      email: '',
      password: '',
      confirmPassword: '',
    },
    validationRules
  );

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (form.validateAll()) {
      // フォーム送信処理
      console.log('Form submitted:', form.values);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type='text'
          placeholder='名前'
          value={form.values.name}
          onChange={(e) =>
            form.handleChange('name', e.target.value)
          }
          onBlur={() => form.handleBlur('name')}
        />
        {form.touched.name && form.errors.name && (
          <span className='error'>{form.errors.name}</span>
        )}
      </div>

      <button type='submit' disabled={!form.isValid}>
        登録
      </button>
    </form>
  );
};

レシピ 11: API 通信の実装

実際のアプリケーションでは、効率的で安全な API 通信の実装が必要です。

型安全な API クライアント

typescript// API レスポンスの基本型
interface ApiResponse<T> {
  data: T;
  message: string;
  success: boolean;
}

// API エラーの型定義
interface ApiError {
  message: string;
  status: number;
  errors?: Record<string, string[]>;
}

// API クライアントクラス
class ApiClient {
  private baseURL: string;
  private defaultHeaders: HeadersInit;

  constructor(baseURL: string) {
    this.baseURL = baseURL;
    this.defaultHeaders = {
      'Content-Type': 'application/json',
    };
  }

  private async request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const url = `${this.baseURL}${endpoint}`;
    const token = localStorage.getItem('authToken');

    const headers = {
      ...this.defaultHeaders,
      ...(token && { Authorization: `Bearer ${token}` }),
      ...options.headers,
    };

    try {
      const response = await fetch(url, {
        ...options,
        headers,
      });

      if (!response.ok) {
        const errorData = await response
          .json()
          .catch(() => ({}));
        throw new ApiError({
          message:
            errorData.message || 'APIエラーが発生しました',
          status: response.status,
          errors: errorData.errors,
        });
      }

      return await response.json();
    } catch (error) {
      if (error instanceof ApiError) {
        throw error;
      }
      throw new ApiError({
        message: 'ネットワークエラーが発生しました',
        status: 0,
      });
    }
  }

  async get<T>(endpoint: string): Promise<T> {
    return this.request<T>(endpoint, { method: 'GET' });
  }

  async post<T>(
    endpoint: string,
    data: unknown
  ): Promise<T> {
    return this.request<T>(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  async put<T>(
    endpoint: string,
    data: unknown
  ): Promise<T> {
    return this.request<T>(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  async delete<T>(endpoint: string): Promise<T> {
    return this.request<T>(endpoint, { method: 'DELETE' });
  }
}

// API クライアントのインスタンス
export const apiClient = new ApiClient('/api');

// React Query を使った API 通信フック
const useUsers = () => {
  return useQuery({
    queryKey: ['users'],
    queryFn: () =>
      apiClient.get<ApiResponse<User[]>>('/users'),
    select: (response) => response.data,
  });
};

const useCreateUser = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (userData: Omit<User, 'id'>) =>
      apiClient.post<ApiResponse<User>>('/users', userData),
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ['users'],
      });
    },
  });
};

レシピ 12: ルーティングの設定

React Router を使用したルーティング設定は、SPA 開発の基本です。

包括的なルーティング設定

typescript// ルート定義の型
interface Route {
  path: string;
  element: React.ComponentType;
  private?: boolean;
  children?: Route[];
}

// プライベートルートコンポーネント
const PrivateRoute: React.FC<{
  children: React.ReactNode;
}> = ({ children }) => {
  const { user, loading } = useAuth();
  const location = useLocation();

  if (loading) {
    return <LoadingSpinner />;
  }

  if (!user) {
    return (
      <Navigate
        to='/login'
        state={{ from: location }}
        replace
      />
    );
  }

  return <>{children}</>;
};

// ルート設定
const routes: Route[] = [
  {
    path: '/',
    element: Home,
  },
  {
    path: '/login',
    element: Login,
  },
  {
    path: '/dashboard',
    element: Dashboard,
    private: true,
    children: [
      {
        path: 'profile',
        element: Profile,
      },
      {
        path: 'settings',
        element: Settings,
      },
    ],
  },
  {
    path: '/admin',
    element: AdminLayout,
    private: true,
    children: [
      {
        path: 'users',
        element: UserManagement,
      },
      {
        path: 'analytics',
        element: Analytics,
      },
    ],
  },
];

// ルート生成関数
const generateRoutes = (
  routes: Route[]
): React.ReactElement => {
  return (
    <Routes>
      {routes.map((route) => {
        const Element = route.element;
        const element = route.private ? (
          <PrivateRoute>
            <Element />
          </PrivateRoute>
        ) : (
          <Element />
        );

        if (route.children) {
          return (
            <Route
              key={route.path}
              path={route.path}
              element={element}
            >
              {route.children.map((child) => (
                <Route
                  key={child.path}
                  path={child.path}
                  element={<child.element />}
                />
              ))}
            </Route>
          );
        }

        return (
          <Route
            key={route.path}
            path={route.path}
            element={element}
          />
        );
      })}
      <Route path='*' element={<NotFound />} />
    </Routes>
  );
};

// アプリケーションのルーター設定
const AppRouter: React.FC = () => {
  return (
    <BrowserRouter>
      <div className='app'>
        <Header />
        <main>{generateRoutes(routes)}</main>
        <Footer />
      </div>
    </BrowserRouter>
  );
};

レシピ 13: エラーバウンダリの実装

エラーバウンダリは、アプリケーションの安定性を保つために重要なコンポーネントです。

包括的なエラーバウンダリ

typescript// エラー情報の型定義
interface ErrorInfo {
  componentStack: string;
  errorBoundary?: string;
  errorBoundaryStack?: string;
}

interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
  errorInfo: ErrorInfo | null;
}

// エラーバウンダリクラス
class ErrorBoundary extends React.Component<
  {
    children: React.ReactNode;
    fallback?: React.ComponentType<{ error: Error }>;
  },
  ErrorBoundaryState
> {
  constructor(props: any) {
    super(props);
    this.state = {
      hasError: false,
      error: null,
      errorInfo: null,
    };
  }

  static getDerivedStateFromError(
    error: Error
  ): Partial<ErrorBoundaryState> {
    return {
      hasError: true,
      error,
    };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    this.setState({
      error,
      errorInfo,
    });

    // エラーログの送信
    this.logError(error, errorInfo);
  }

  private logError = (
    error: Error,
    errorInfo: ErrorInfo
  ) => {
    const errorReport = {
      message: error.message,
      stack: error.stack,
      componentStack: errorInfo.componentStack,
      timestamp: new Date().toISOString(),
      userAgent: navigator.userAgent,
      url: window.location.href,
    };

    // ログサービスに送信
    fetch('/api/error-reports', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(errorReport),
    }).catch((err) => {
      console.error('Failed to log error:', err);
    });
  };

  render() {
    if (this.state.hasError) {
      const { fallback: Fallback } = this.props;

      if (Fallback && this.state.error) {
        return <Fallback error={this.state.error} />;
      }

      return (
        <div className='error-boundary'>
          <h2>予期しないエラーが発生しました</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            <summary>エラー詳細</summary>
            {this.state.error &&
              this.state.error.toString()}
            <br />
            {this.state.errorInfo?.componentStack}
          </details>
          <button onClick={() => window.location.reload()}>
            ページを再読み込み
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// カスタムエラーフォールバック
const CustomErrorFallback: React.FC<{ error: Error }> = ({
  error,
}) => {
  const [isReporting, setIsReporting] = useState(false);

  const reportError = async () => {
    setIsReporting(true);
    try {
      await fetch('/api/user-reports', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          error: error.message,
          timestamp: new Date().toISOString(),
        }),
      });
      alert('エラーレポートを送信しました');
    } catch (err) {
      alert('レポートの送信に失敗しました');
    } finally {
      setIsReporting(false);
    }
  };

  return (
    <div className='error-fallback'>
      <h2>問題が発生しました</h2>
      <p>
        申し訳ございませんが、予期しないエラーが発生しました。
      </p>
      <div className='error-actions'>
        <button onClick={() => window.location.reload()}>
          ページを再読み込み
        </button>
        <button
          onClick={reportError}
          disabled={isReporting}
        >
          {isReporting ? '送信中...' : 'エラーを報告'}
        </button>
      </div>
    </div>
  );
};

// 使用例
const App: React.FC = () => {
  return (
    <ErrorBoundary fallback={CustomErrorFallback}>
      <AuthProvider>
        <AppRouter />
      </AuthProvider>
    </ErrorBoundary>
  );
};

レシピ 14: パフォーマンス最適化

React アプリケーションのパフォーマンス最適化は、ユーザー体験向上のために不可欠です。

メモ化による最適化

typescript// React.memo による最適化
interface ProductCardProps {
  product: Product;
  onAddToCart: (productId: string) => void;
}

const ProductCard: React.FC<ProductCardProps> = React.memo(
  ({ product, onAddToCart }) => {
    const handleAddToCart = useCallback(() => {
      onAddToCart(product.id);
    }, [product.id, onAddToCart]);

    return (
      <div className='product-card'>
        <img src={product.image} alt={product.name} />
        <h3>{product.name}</h3>
        <p>¥{product.price.toLocaleString()}</p>
        <button onClick={handleAddToCart}>
          カートに追加
        </button>
      </div>
    );
  }
);

// useMemo による計算結果のメモ化
const ProductList: React.FC<{
  products: Product[];
  searchQuery: string;
}> = ({ products, searchQuery }) => {
  const filteredProducts = useMemo(() => {
    if (!searchQuery) return products;

    return products.filter(
      (product) =>
        product.name
          .toLowerCase()
          .includes(searchQuery.toLowerCase()) ||
        product.description
          .toLowerCase()
          .includes(searchQuery.toLowerCase())
    );
  }, [products, searchQuery]);

  const sortedProducts = useMemo(() => {
    return [...filteredProducts].sort(
      (a, b) => b.rating - a.rating
    );
  }, [filteredProducts]);

  const onAddToCart = useCallback((productId: string) => {
    // カート追加ロジック
    console.log('Added to cart:', productId);
  }, []);

  return (
    <div className='product-list'>
      {sortedProducts.map((product) => (
        <ProductCard
          key={product.id}
          product={product}
          onAddToCart={onAddToCart}
        />
      ))}
    </div>
  );
};

// 仮想化による大量データの最適化
const VirtualizedList: React.FC<{
  items: any[];
  renderItem: (item: any, index: number) => React.ReactNode;
}> = ({ items, renderItem }) => {
  const [startIndex, setStartIndex] = useState(0);
  const [endIndex, setEndIndex] = useState(50);
  const containerRef = useRef<HTMLDivElement>(null);

  const ITEM_HEIGHT = 100;
  const VISIBLE_ITEMS = 10;

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    const handleScroll = () => {
      const scrollTop = container.scrollTop;
      const newStartIndex = Math.floor(
        scrollTop / ITEM_HEIGHT
      );
      const newEndIndex = Math.min(
        newStartIndex + VISIBLE_ITEMS,
        items.length
      );

      setStartIndex(newStartIndex);
      setEndIndex(newEndIndex);
    };

    container.addEventListener('scroll', handleScroll);
    return () =>
      container.removeEventListener('scroll', handleScroll);
  }, [items.length]);

  const visibleItems = items.slice(startIndex, endIndex);
  const totalHeight = items.length * ITEM_HEIGHT;
  const offsetY = startIndex * ITEM_HEIGHT;

  return (
    <div
      ref={containerRef}
      className='virtualized-list'
      style={{ height: '500px', overflow: 'auto' }}
    >
      <div
        style={{
          height: totalHeight,
          position: 'relative',
        }}
      >
        <div
          style={{ transform: `translateY(${offsetY}px)` }}
        >
          {visibleItems.map((item, index) => (
            <div
              key={startIndex + index}
              style={{ height: ITEM_HEIGHT }}
            >
              {renderItem(item, startIndex + index)}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

中級編で学んだパターンは、実際のプロダクション環境で重要となる実践的なスキルです。これらのレシピを活用することで、スケーラブルで保守性の高い React アプリケーションを構築できるようになるでしょう。

応用編(レシピ 15-20)

応用編では、プロ級の React 開発スキルを GitHub Copilot と共に習得していきます。テスト自動化、開発ツール連携、高度な最適化手法など、エンタープライズレベルのアプリケーション開発に必要な技術をマスターしていただけます。

レシピ 15: テストコードの自動生成

いつ使うか

  • 新しく作成したコンポーネントのテストを書くとき
  • 既存コードのリファクタリング前に安全網としてテストを追加したいとき
  • CI/CD パイプラインでテストカバレッジを向上させたいとき

品質の高いアプリケーションを構築するためには、包括的なテスト戦略が不可欠です。GitHub Copilot を活用することで、効率的にテストコードを生成できます。手動でテストを書く時間を大幅に短縮し、より多くのテストケースをカバーできるようになります。

ユニットテストの自動生成

typescript// テスト対象のコンポーネント
interface UserCardProps {
  user: {
    id: string;
    name: string;
    email: string;
  };
  onEdit: (userId: string) => void;
  onDelete: (userId: string) => void;
}

const UserCard: React.FC<UserCardProps> = ({
  user,
  onEdit,
  onDelete,
}) => {
  return (
    <div data-testid='user-card'>
      <h3 data-testid='user-name'>{user.name}</h3>
      <p data-testid='user-email'>{user.email}</p>
      <button
        data-testid='edit-button'
        onClick={() => onEdit(user.id)}
      >
        編集
      </button>
      <button
        data-testid='delete-button'
        onClick={() => onDelete(user.id)}
      >
        削除
      </button>
    </div>
  );
};

// GitHub Copilot により自動生成されるテストコード
import {
  render,
  screen,
  fireEvent,
} from '@testing-library/react';
import '@testing-library/jest-dom';
import { UserCard } from './UserCard';

describe('UserCard コンポーネント', () => {
  const mockUser = {
    id: '1',
    name: '山田太郎',
    email: 'yamada@example.com',
  };

  const mockOnEdit = jest.fn();
  const mockOnDelete = jest.fn();

  beforeEach(() => {
    jest.clearAllMocks();
  });

  test('ユーザー情報が正しく表示される', () => {
    render(
      <UserCard
        user={mockUser}
        onEdit={mockOnEdit}
        onDelete={mockOnDelete}
      />
    );

    expect(
      screen.getByTestId('user-name')
    ).toHaveTextContent('山田太郎');
    expect(
      screen.getByTestId('user-email')
    ).toHaveTextContent('yamada@example.com');
  });

  test('編集ボタンクリック時にonEditが呼ばれる', () => {
    render(
      <UserCard
        user={mockUser}
        onEdit={mockOnEdit}
        onDelete={mockOnDelete}
      />
    );

    fireEvent.click(screen.getByTestId('edit-button'));
    expect(mockOnEdit).toHaveBeenCalledWith('1');
    expect(mockOnEdit).toHaveBeenCalledTimes(1);
  });

  test('削除ボタンクリック時にonDeleteが呼ばれる', () => {
    render(
      <UserCard
        user={mockUser}
        onEdit={mockOnEdit}
        onDelete={mockOnDelete}
      />
    );

    fireEvent.click(screen.getByTestId('delete-button'));
    expect(mockOnDelete).toHaveBeenCalledWith('1');
    expect(mockOnDelete).toHaveBeenCalledTimes(1);
  });
});

統合テストの実装

typescript// API 統合テストの自動生成
import {
  renderHook,
  waitFor,
} from '@testing-library/react';
import {
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query';
import { useFetch } from './useFetch';

// モックサーバーのセットアップ
import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/users/1', (req, res, ctx) => {
    return res(
      ctx.json({
        id: '1',
        name: '山田太郎',
        email: 'yamada@example.com',
      })
    );
  }),

  rest.get('/api/users/error', (req, res, ctx) => {
    return res(
      ctx.status(500),
      ctx.json({ message: 'Server Error' })
    );
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('useFetch カスタムフック', () => {
  const createWrapper = () => {
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: { retry: false },
        mutations: { retry: false },
      },
    });

    return ({
      children,
    }: {
      children: React.ReactNode;
    }) => (
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    );
  };

  test('データの取得が成功する', async () => {
    const { result } = renderHook(
      () => useFetch('/api/users/1'),
      { wrapper: createWrapper() }
    );

    expect(result.current.loading).toBe(true);
    expect(result.current.data).toBe(null);

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.data).toEqual({
      id: '1',
      name: '山田太郎',
      email: 'yamada@example.com',
    });
    expect(result.current.error).toBe(null);
  });

  test('エラーハンドリングが正しく動作する', async () => {
    const { result } = renderHook(
      () => useFetch('/api/users/error'),
      { wrapper: createWrapper() }
    );

    expect(result.current.loading).toBe(true);

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.data).toBe(null);
    expect(result.current.error).toBe(
      'HTTP error! status: 500'
    );
  });
});

レシピ 16: Storybook の設定

Storybook は、コンポーネントの開発とドキュメント化を効率化する重要なツールです。

Storybook ストーリーの自動生成

typescript// Button コンポーネントのストーリー
import type { Meta, StoryObj } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  title: 'Example/Button',
  component: Button,
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: { type: 'select' },
      options: ['primary', 'secondary', 'danger'],
    },
    size: {
      control: { type: 'select' },
      options: ['small', 'medium', 'large'],
    },
    disabled: {
      control: { type: 'boolean' },
    },
  },
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
  args: {
    variant: 'primary',
    children: 'プライマリボタン',
    onClick: action('clicked'),
  },
};

export const Secondary: Story = {
  args: {
    variant: 'secondary',
    children: 'セカンダリボタン',
    onClick: action('clicked'),
  },
};

export const Danger: Story = {
  args: {
    variant: 'danger',
    children: '危険なアクション',
    onClick: action('clicked'),
  },
};

export const Small: Story = {
  args: {
    size: 'small',
    children: '小さいボタン',
    onClick: action('clicked'),
  },
};

export const Large: Story = {
  args: {
    size: 'large',
    children: '大きいボタン',
    onClick: action('clicked'),
  },
};

export const Disabled: Story = {
  args: {
    disabled: true,
    children: '無効化されたボタン',
    onClick: action('clicked'),
  },
};

高度なストーリー設定

typescript// フォームコンポーネントの複雑なストーリー
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within, expect } from '@storybook/test';
import { UserRegistrationForm } from './UserRegistrationForm';

const meta: Meta<typeof UserRegistrationForm> = {
  title: 'Forms/UserRegistrationForm',
  component: UserRegistrationForm,
  parameters: {
    layout: 'centered',
  },
};

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {};

export const WithValidationErrors: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // 無効なデータを入力
    await userEvent.type(
      canvas.getByLabelText('名前'),
      'a'
    );
    await userEvent.type(
      canvas.getByLabelText('メール'),
      'invalid-email'
    );
    await userEvent.type(
      canvas.getByLabelText('パスワード'),
      '123'
    );

    // フォーカスを外してバリデーションを発火
    await userEvent.tab();

    // エラーメッセージの確認
    await expect(
      canvas.getByText('2文字以上で入力してください')
    ).toBeInTheDocument();
    await expect(
      canvas.getByText('正しい形式で入力してください')
    ).toBeInTheDocument();
    await expect(
      canvas.getByText('8文字以上で入力してください')
    ).toBeInTheDocument();
  },
};

export const SuccessfulSubmission: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // 有効なデータを入力
    await userEvent.type(
      canvas.getByLabelText('名前'),
      '山田太郎'
    );
    await userEvent.type(
      canvas.getByLabelText('メール'),
      'yamada@example.com'
    );
    await userEvent.type(
      canvas.getByLabelText('パスワード'),
      'securepassword123'
    );
    await userEvent.type(
      canvas.getByLabelText('パスワード確認'),
      'securepassword123'
    );

    // 送信ボタンをクリック
    await userEvent.click(
      canvas.getByRole('button', { name: '登録' })
    );

    // 成功メッセージの確認
    await expect(
      canvas.getByText('登録が完了しました')
    ).toBeInTheDocument();
  },
};

レシピ 17: 複雑な状態管理

大規模アプリケーションでは、複雑な状態管理が重要になります。Redux Toolkit や Zustand を活用した実装を学びましょう。

Redux Toolkit による状態管理

typescript// ユーザー管理のスライス
import {
  createSlice,
  createAsyncThunk,
  PayloadAction,
} from '@reduxjs/toolkit';

interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

interface UserState {
  users: User[];
  currentUser: User | null;
  loading: boolean;
  error: string | null;
}

const initialState: UserState = {
  users: [],
  currentUser: null,
  loading: false,
  error: null,
};

// 非同期アクション
export const fetchUsers = createAsyncThunk(
  'users/fetchUsers',
  async (_, { rejectWithValue }) => {
    try {
      const response = await fetch('/api/users');
      if (!response.ok) {
        throw new Error('ユーザー取得に失敗しました');
      }
      return await response.json();
    } catch (error) {
      return rejectWithValue(
        error instanceof Error
          ? error.message
          : '予期しないエラー'
      );
    }
  }
);

export const createUser = createAsyncThunk(
  'users/createUser',
  async (
    userData: Omit<User, 'id'>,
    { rejectWithValue }
  ) => {
    try {
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData),
      });

      if (!response.ok) {
        throw new Error('ユーザー作成に失敗しました');
      }

      return await response.json();
    } catch (error) {
      return rejectWithValue(
        error instanceof Error
          ? error.message
          : '予期しないエラー'
      );
    }
  }
);

const userSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {
    setCurrentUser: (
      state,
      action: PayloadAction<User | null>
    ) => {
      state.currentUser = action.payload;
    },
    updateUser: (state, action: PayloadAction<User>) => {
      const index = state.users.findIndex(
        (user) => user.id === action.payload.id
      );
      if (index !== -1) {
        state.users[index] = action.payload;
      }
    },
    deleteUser: (state, action: PayloadAction<string>) => {
      state.users = state.users.filter(
        (user) => user.id !== action.payload
      );
    },
    clearError: (state) => {
      state.error = null;
    },
  },
  extraReducers: (builder) => {
    builder
      // fetchUsers
      .addCase(fetchUsers.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.loading = false;
        state.users = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload as string;
      })
      // createUser
      .addCase(createUser.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(createUser.fulfilled, (state, action) => {
        state.loading = false;
        state.users.push(action.payload);
      })
      .addCase(createUser.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload as string;
      });
  },
});

export const {
  setCurrentUser,
  updateUser,
  deleteUser,
  clearError,
} = userSlice.actions;
export default userSlice.reducer;

// セレクター
export const selectUsers = (state: { users: UserState }) =>
  state.users.users;
export const selectCurrentUser = (state: {
  users: UserState;
}) => state.users.currentUser;
export const selectUsersLoading = (state: {
  users: UserState;
}) => state.users.loading;
export const selectUsersError = (state: {
  users: UserState;
}) => state.users.error;

Zustand による軽量な状態管理

typescript// Zustand を使用したシンプルな状態管理
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartState {
  items: CartItem[];
  total: number;
  addItem: (item: Omit<CartItem, 'quantity'>) => void;
  removeItem: (id: string) => void;
  updateQuantity: (id: string, quantity: number) => void;
  clearCart: () => void;
  calculateTotal: () => void;
}

export const useCartStore = create<CartState>()(
  devtools(
    persist(
      (set, get) => ({
        items: [],
        total: 0,

        addItem: (newItem) => {
          const items = get().items;
          const existingItem = items.find(
            (item) => item.id === newItem.id
          );

          if (existingItem) {
            set((state) => ({
              items: state.items.map((item) =>
                item.id === newItem.id
                  ? { ...item, quantity: item.quantity + 1 }
                  : item
              ),
            }));
          } else {
            set((state) => ({
              items: [
                ...state.items,
                { ...newItem, quantity: 1 },
              ],
            }));
          }

          get().calculateTotal();
        },

        removeItem: (id) => {
          set((state) => ({
            items: state.items.filter(
              (item) => item.id !== id
            ),
          }));
          get().calculateTotal();
        },

        updateQuantity: (id, quantity) => {
          if (quantity <= 0) {
            get().removeItem(id);
            return;
          }

          set((state) => ({
            items: state.items.map((item) =>
              item.id === id ? { ...item, quantity } : item
            ),
          }));
          get().calculateTotal();
        },

        clearCart: () => {
          set({ items: [], total: 0 });
        },

        calculateTotal: () => {
          const items = get().items;
          const total = items.reduce(
            (sum, item) => sum + item.price * item.quantity,
            0
          );
          set({ total });
        },
      }),
      {
        name: 'cart-storage',
        partialize: (state) => ({ items: state.items }),
      }
    ),
    { name: 'cart-store' }
  )
);

// 使用例
const ShoppingCart: React.FC = () => {
  const {
    items,
    total,
    addItem,
    removeItem,
    updateQuantity,
    clearCart,
  } = useCartStore();

  return (
    <div className='shopping-cart'>
      <h2>ショッピングカート</h2>

      {items.map((item) => (
        <div key={item.id} className='cart-item'>
          <span>{item.name}</span>
          <span>¥{item.price}</span>
          <input
            type='number'
            value={item.quantity}
            onChange={(e) =>
              updateQuantity(
                item.id,
                parseInt(e.target.value)
              )
            }
          />
          <button onClick={() => removeItem(item.id)}>
            削除
          </button>
        </div>
      ))}

      <div className='cart-total'>合計: ¥{total}</div>

      <button onClick={clearCart}>カートをクリア</button>
    </div>
  );
};

レシピ 18: サーバーサイドレンダリング

Next.js を使用したサーバーサイドレンダリングの実装は、SEO とパフォーマンス向上のために重要です。

Next.js の App Router を活用した SSR

typescript// app/users/[id]/page.tsx
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { UserProfile } from '@/components/UserProfile';

interface PageProps {
  params: { id: string };
}

// ユーザーデータの取得
async function getUser(id: string) {
  const res = await fetch(
    `${process.env.API_BASE_URL}/users/${id}`,
    {
      next: { revalidate: 60 }, // 60秒キャッシュ
    }
  );

  if (!res.ok) {
    if (res.status === 404) {
      notFound();
    }
    throw new Error('Failed to fetch user');
  }

  return res.json();
}

// メタデータの動的生成
export async function generateMetadata({
  params,
}: PageProps): Promise<Metadata> {
  try {
    const user = await getUser(params.id);

    return {
      title: `${user.name} - ユーザープロフィール`,
      description: `${user.name}さんのプロフィールページです。`,
      openGraph: {
        title: `${user.name} - ユーザープロフィール`,
        description: `${user.name}さんのプロフィールページです。`,
        images: [user.avatar],
      },
    };
  } catch {
    return {
      title: 'ユーザーが見つかりません',
    };
  }
}

// 静的パラメータの生成(ISGの場合)
export async function generateStaticParams() {
  const res = await fetch(
    `${process.env.API_BASE_URL}/users`
  );
  const users = await res.json();

  return users.map((user: { id: string }) => ({
    id: user.id,
  }));
}

// ページコンポーネント
export default async function UserPage({
  params,
}: PageProps) {
  const user = await getUser(params.id);

  return (
    <div className='user-page'>
      <UserProfile user={user} />
    </div>
  );
}

カスタムサーバーとミドルウェア

typescript// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // 認証チェック
  const token = request.cookies.get('auth-token');
  const isAuthPage =
    request.nextUrl.pathname.startsWith('/auth');
  const isProtectedRoute =
    request.nextUrl.pathname.startsWith('/dashboard');

  // 認証が必要なページで未認証の場合
  if (isProtectedRoute && !token) {
    return NextResponse.redirect(
      new URL('/auth/login', request.url)
    );
  }

  // 認証済みユーザーが認証ページにアクセスした場合
  if (isAuthPage && token) {
    return NextResponse.redirect(
      new URL('/dashboard', request.url)
    );
  }

  // レート制限
  const ip = request.ip ?? '127.0.0.1';
  const rateLimitKey = `rate-limit:${ip}`;

  // Redis等を使用したレート制限の実装
  // 簡略化のため詳細は省略

  return NextResponse.next();
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

レシピ 19: TypeScript の型安全性向上

高度な TypeScript の活用により、コードの安全性と開発効率を向上させます。

高度な型定義とユーティリティ型

typescript// 高度な型定義の例
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? DeepReadonly<T[P]>
    : T[P];
};

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object
    ? DeepPartial<T[P]>
    : T[P];
};

// API レスポンスの型安全な処理
interface ApiSuccess<T> {
  success: true;
  data: T;
  message?: string;
}

interface ApiError {
  success: false;
  error: string;
  details?: Record<string, string[]>;
}

type ApiResponse<T> = ApiSuccess<T> | ApiError;

// 型ガード関数
function isApiSuccess<T>(
  response: ApiResponse<T>
): response is ApiSuccess<T> {
  return response.success === true;
}

// 型安全な API クライアント
class TypeSafeApiClient {
  async request<T>(
    endpoint: string,
    options?: RequestInit
  ): Promise<T> {
    const response = await fetch(endpoint, options);
    const data: ApiResponse<T> = await response.json();

    if (isApiSuccess(data)) {
      return data.data;
    } else {
      throw new Error(data.error);
    }
  }

  async get<T>(endpoint: string): Promise<T> {
    return this.request<T>(endpoint, { method: 'GET' });
  }

  async post<T, U = unknown>(
    endpoint: string,
    body: U
  ): Promise<T> {
    return this.request<T>(endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    });
  }
}

// フォーム状態の型安全な管理
type FormErrors<T> = {
  [K in keyof T]?: string;
};

type FormTouched<T> = {
  [K in keyof T]?: boolean;
};

interface FormState<T> {
  values: T;
  errors: FormErrors<T>;
  touched: FormTouched<T>;
  isValid: boolean;
  isSubmitting: boolean;
}

// 型安全なフォームフック
function useTypeSafeForm<T extends Record<string, any>>(
  initialValues: T,
  validationSchema: ValidationSchema<T>
): FormState<T> & FormActions<T> {
  const [values, setValues] = useState<T>(initialValues);
  const [errors, setErrors] = useState<FormErrors<T>>({});
  const [touched, setTouched] = useState<FormTouched<T>>(
    {}
  );
  const [isSubmitting, setIsSubmitting] = useState(false);

  const validateField = useCallback(
    <K extends keyof T>(
      field: K,
      value: T[K]
    ): string | undefined => {
      const rule = validationSchema[field];
      if (rule) {
        return rule(value);
      }
      return undefined;
    },
    [validationSchema]
  );

  const setValue = useCallback(
    <K extends keyof T>(field: K, value: T[K]) => {
      setValues((prev) => ({ ...prev, [field]: value }));

      if (touched[field]) {
        const error = validateField(field, value);
        setErrors((prev) => ({ ...prev, [field]: error }));
      }
    },
    [touched, validateField]
  );

  const setFieldTouched = useCallback(
    <K extends keyof T>(field: K) => {
      setTouched((prev) => ({ ...prev, [field]: true }));
      const error = validateField(field, values[field]);
      setErrors((prev) => ({ ...prev, [field]: error }));
    },
    [values, validateField]
  );

  const isValid = useMemo(() => {
    return Object.values(errors).every((error) => !error);
  }, [errors]);

  return {
    values,
    errors,
    touched,
    isValid,
    isSubmitting,
    setValue,
    setFieldTouched,
    setIsSubmitting,
  };
}

レシピ 20: デバッグとトラブルシューティング

効率的なデバッグ手法とパフォーマンス監視は、品質の高いアプリケーション開発に不可欠です。

React DevTools との連携

typescript// React DevTools Profiler API の活用
import { Profiler, ProfilerOnRenderCallback } from 'react';

const onRenderCallback: ProfilerOnRenderCallback = (
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime,
  interactions
) => {
  // パフォーマンスデータをログ出力
  console.log('Profiler:', {
    id,
    phase,
    actualDuration,
    baseDuration,
    startTime,
    commitTime,
    interactions,
  });

  // パフォーマンス監視サービスにデータ送信
  if (actualDuration > 100) { // 100ms以上の場合
    sendPerformanceData({
      componentId: id,
      renderTime: actualDuration,
      phase,
      timestamp: Date.now(),
    });
  }
};

const App: React.FC = () => {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <Router>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </Router>
    </Profiler>
  );
};

// カスタムデバッグフック
const useDebugValue = <T>(value: T, formatter?: (value: T) => any) => {
  useDebugValue(value, formatter);
  return value;
};

const useDebugRender = (name: string, props: Record<string, any>) => {
  const prevProps = useRef(props);

  useEffect(() => {
    const changedProps = Object.keys(props).reduce((acc, key) => {
      if (prevProps.current[key] !== props[key]) {
        acc[key] = {
          from: prevProps.current[key],
          to: props[key],
        };
      }
      return acc;
    }, {} as Record<string, any>);

    if (Object.keys(changedProps).length > 0) {
      console.log(`${name} re-rendered. Changed props:`, changedProps);
    }

    prevProps.current = props;
  });
};

// エラー境界とログ収集
const ErrorLogger = {
  logError: (error: Error, errorInfo?: { componentStack: string }) => {
    const errorData = {
      message: error.message,
      stack: error.stack,
      componentStack: errorInfo?.componentStack,
      userAgent: navigator.userAgent,
      url: window.location.href,
      timestamp: new Date().toISOString(),
      userId: getCurrentUserId(),
    };

    // ローカルストレージに保存(オフライン対応)
    const errorLogs = JSON.parse(localStorage.getItem('errorLogs') || '[]');
    errorLogs.push(errorData);
    localStorage.setItem('errorLogs', JSON.stringify(errorLogs.slice(-50))); // 最新50件

    // サーバーに送信
    fetch('/api/error-logs', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(errorData),
    }).catch(() => {
      // 送信失敗時はコンソールにログ出力
      console.error('Failed to send error log:', errorData);
    });
  },

  getStoredErrors: () => {
    return JSON.parse(localStorage.getItem('errorLogs') || '[]');
  },

  clearStoredErrors: () => {
    localStorage.removeItem('errorLogs');
  },
};

// パフォーマンス監視
const PerformanceMonitor = {
  measureRenderTime: (componentName: string, renderFn: () => void) => {
    const startTime = performance.now();
    renderFn();
    const endTime = performance.now();

    const renderTime = endTime - startTime;

    if (renderTime > 16) { // 60FPSを下回る場合
      console.warn(`${componentName} took ${renderTime.toFixed(2)}ms to render`);
    }
  },

  trackUserInteraction: (action: string, element: string) => {
    const startTime = performance.now();

    return () => {
      const endTime = performance.now();
      const duration = endTime - startTime;

      // ユーザーインタラクションの追跡
      sendAnalytics({
        event: 'user_interaction',
        action,
        element,
        duration,
        timestamp: Date.now(),
      });
    };
  },
};

応用編で学んだテクニックは、エンタープライズレベルの React アプリケーション開発において不可欠なスキルです。これらのレシピを活用することで、プロダクション環境での安定性、保守性、パフォーマンスを大幅に向上させることができるでしょう。

実践!明日から使える導入ステップ

ステップ1: GitHub Copilot のセットアップ(5分)

  1. GitHub Copilot のサブスクリプション開始

    • GitHub Copilot にアクセス
    • 無料体験または有料プランを選択
  2. エディタへの拡張機能インストール

    • VS Code: 「GitHub Copilot」拡張機能をインストール
    • JetBrains IDEs: GitHub Copilot プラグインを追加
  3. 認証設定

    • エディタで GitHub アカウントにログイン
    • 利用規約に同意

ステップ2: 最初の一週間(基礎レシピの習得)

Day 1-2: コンポーネント作成をマスター

  • レシピ1を使って新しいコンポーネントを3つ作成
  • コメントの書き方のコツを掴む

Day 3-4: 状態管理とフックを練習

  • レシピ2-3を使ってフォームコンポーネントを作成
  • useState と useEffect の組み合わせパターンを習得

Day 5-7: TypeScript 活用と UI ロジック

  • レシピ4-7を使って型安全なコンポーネントを作成
  • 条件分岐とリスト表示をマスター

ステップ3: 二週間目(中級レシピの実践)

実践プロジェクト: TodoアプリやブログSPAを作成

  • カスタムフック(レシピ8)でロジックを共通化
  • Context API(レシピ9)でグローバル状態管理
  • フォームバリデーション(レシピ10)でユーザビリティ向上

ステップ4: チームでの共有と標準化

チーム向けガイドライン作成

markdown# 我がチームの GitHub Copilot 活用ルール

## コメント記述規約
- 機能: 何をするコンポーネント/関数か
- プロップス/引数: どんなデータを受け取るか
- 責務: どんな責任を持つか

## レビュー観点
- Copilot提案コードの品質確認
- プロジェクト固有の命名規則適用
- セキュリティリスクの検証

よくある導入時の課題と解決法

課題1: 「思った通りのコードが生成されない」 解決法: コメントをより具体的に書く。「何を」「どのように」「なぜ」を明記する

課題2: 「生成されたコードが複雑すぎる」 解決法: 段階的にアプローチする。まず基本機能から始めて、徐々に拡張する

課題3: 「チームで使い方がバラバラ」 解決法: 共通のコメント規約を策定し、レビュー時にチェックする

成果測定の指標

導入効果を測定するための KPI:

  • 開発時間: コンポーネント作成時間の短縮率
  • コード品質: レビュー指摘事項の減少
  • 学習効果: 新しいパターンの習得数
  • チーム満足度: 開発者の GitHub Copilot 満足度調査

まとめ

GitHub Copilot 活用のベストプラクティス

本記事でご紹介した 20 のレシピを通じて、GitHub Copilot を活用した React 開発の効率化手法を学んでいただきました。ここで、これらの知見を最大限に活用するためのベストプラクティスをまとめてご紹介いたします。

効果的な Copilot 活用の原則

明確な意図の表現 Copilot への指示は、コメントで具体的かつ明確に表現することが重要です。「何を」「どのように」「なぜ」作るのかを詳述することで、より適切な提案を得ることができます。

typescript// 良い例:具体的で明確な指示
// ユーザー一覧を表示するテーブルコンポーネント
// 機能:ソート、フィルタリング、ページング対応
// プロップス:users配列、onUserSelect関数
// スタイル:Material-UI風のデザイン

// 悪い例:曖昧な指示
// ユーザーリスト

段階的な実装アプローチ 複雑な機能を一度に実装しようとせず、段階的にアプローチすることで、より精度の高い提案を得られます。基本機能から始めて、徐々に高度な機能を追加していく方法が効果的です。

コンテキストの活用 既存のコードやプロジェクトの構造を Copilot が理解できるよう、関連するファイルを開いた状態で作業することで、より適切な提案を受けることができます。

開発効率最大化のための戦略

以下の図は、GitHub Copilot を活用した効率的な開発フローを示しています。

mermaidflowchart TD
    A[要件定義] --> B[コメントによる設計]
    B --> C[Copilot による提案]
    C --> D[コード選択・調整]
    D --> E[テスト実行]
    E --> F{品質確認}
    F -->|OK| G[次の機能へ]
    F -->|NG| H[リファクタリング]
    H --> D
    G --> I[統合テスト]
    I --> J[デプロイ]

    C --> K[複数提案の比較]
    K --> L[最適解の選択]
    L --> D

この循環的なプロセスにより、品質を保ちながら開発速度を向上させることができます。

時間配分の最適化 Copilot を活用することで、以下のような時間配分の最適化が可能になります。

従来の開発作業時間割合Copilot 活用後時間割合
ボイラープレート作成30%ボイラープレート作成5%
機能実装40%機能実装35%
デバッグ20%デバッグ15%
テスト作成10%テスト作成15%
--アーキテクチャ設計20%
--UX/UI 改善10%

表からわかるように、定型的な作業時間が大幅に削減され、より創造的で価値の高い作業に時間を充てることができるようになります。

チーム開発での活用指針

共通パターンの確立 チーム内で Copilot の活用パターンを統一することで、コードの一貫性を保つことができます。プロジェクト固有のコメント規約や命名規則を策定しましょう。

知識共有の促進 Copilot から得られた有用な提案やパターンを、チーム内で積極的に共有することで、全体のスキル向上につながります。

継続的な学習 Copilot の提案を鵜呑みにするのではなく、提案されたコードを理解し、より良い実装方法を学習する姿勢が重要です。

今後の展望

AI 支援開発の進化

GitHub Copilot をはじめとする AI 支援ツールは、今後さらなる進化を遂げることが予想されます。以下のような発展が期待されています。

より高度なコンテキスト理解 プロジェクト全体の設計思想やアーキテクチャを理解し、より適切な提案を行えるようになるでしょう。

自動テスト生成の高度化 単純なユニットテストだけでなく、複雑な統合テストやエンドツーエンドテストの自動生成も可能になると予想されます。

パフォーマンス最適化の自動化 コードのパフォーマンスボトルネックを自動検出し、最適化案を提示する機能が強化されるでしょう。

React エコシステムとの融合

Next.js との連携強化 サーバーサイドレンダリングやエッジ関数の実装において、より効率的な支援が提供されることが期待されます。

状態管理ライブラリとの統合 Redux、Zustand、Jotai などの状態管理ライブラリに特化した提案機能が強化される可能性があります。

TypeScript 型システムとの深い統合 より複雑な型定義や型安全性の確保において、AI による支援が拡充されるでしょう。

開発者スキルの変化

AI 支援開発の普及により、開発者に求められるスキルも変化していきます。

設計力の重要性向上 コード実装の自動化が進むことで、アーキテクチャ設計やシステム設計の能力がより重要になります。

問題解決能力の強化 AI が提案する複数の選択肢から最適解を選択し、カスタマイズする能力が求められます。

継続学習の必要性 技術の進歩が加速する中で、新しいツールや手法を継続的に学習する姿勢がより重要になるでしょう。

今回ご紹介した 20 のレシピは、現在の GitHub Copilot の能力を最大限に活用するためのものです。しかし、AI 技術の進歩と共に、これらの手法もさらに洗練されていくことでしょう。

重要なのは、AI を単なるツールとして使うのではなく、開発パートナーとして協働する姿勢を持つことです。AI の提案を理解し、改善し、自分なりの開発スタイルを確立することで、真の意味での生産性向上を実現できるのです。

GitHub Copilot との協働により、皆さまの React 開発がより楽しく、効率的で、創造的なものになることを心より願っております。ぜひ、今日から実践してみてくださいね。

関連リンク

公式ドキュメント

開発ツール

学習リソース

コミュニティ