React独学は可能?効率的に学ぶための7つのステップ

React独学の完全ガイド。初心者が効率的にReactを習得するための7つのステップと具体的な学習方法を詳しく解説

Learning Next 運営
109 分で読めます

React独学は可能?効率的に学ぶための7つのステップ

みなさん、「Reactを独学で習得できるかな?」と悩んでいませんか?

「どこから始めればいいかわからない」「プログラミングスクールに通わないと無理?」そんな風に思ったことはありませんか?

実は、React独学は十分可能です。 適切な学習プランがあれば、自分のペースで確実にスキルを身につけることができます。

この記事では、React独学の可能性と効率的な学習方法を7つのステップで詳しく解説します。 実際のコード例も交えながら、段階的な学習プランをお伝えしますので、ぜひ参考にしてください。

React独学は本当に可能?

結論から言うと、React独学は十分可能です。

独学成功の条件

React独学を成功させるためには、いくつかの条件があります。

必要な基礎知識

まず、これらの基礎知識があると学習がスムーズに進みます。

  • HTML/CSS: 基本的なマークアップとスタイリング
  • JavaScript: ES6以降の基本文法
  • プログラミング思考: 論理的な問題解決能力
<!-- HTML/CSSの理解度チェック -->
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>React学習準備</title>
  <style>
    .container { max-width: 800px; margin: 0 auto; }
    .button { padding: 10px 20px; background: #007bff; color: white; }
  </style>
</head>
<body>
  <div class="container">
    <h1>React学習開始</h1>
    <button class="button">スタート</button>
  </div>
</body>
</html>

このHTMLコードが理解できれば、HTML/CSSの基礎は大丈夫です。

<div><h1>といったタグの意味、CSSでのスタイリング方法が分かっていることが重要です。

// JavaScriptの理解度チェック
// 基本的な関数と配列操作ができるか
const users = [
  { id: 1, name: "太郎", age: 25 },
  { id: 2, name: "花子", age: 30 }
];

// アロー関数と配列メソッド
const adultUsers = users.filter(user => user.age >= 20);
const userNames = users.map(user => user.name);

// 分割代入
const { name, age } = users[0];

// 非同期処理の基本
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('エラー:', error);
  }
}

このJavaScriptコードを見て理解できれば、Reactの学習を始める準備ができています。

特に重要なのは、アロー関数配列のメソッド分割代入非同期処理です。 これらはReactでよく使う機能なので、しっかり理解しておきましょう。

学習に必要な時間

React独学に必要な時間の目安をお伝えします。

  • 平日: 1-2時間/日
  • 休日: 3-5時間/日
  • 期間: 3-6ヶ月(基礎から実務レベル)

無理をせず、継続できるペースで学習することが大切です。

独学のメリット・デメリット

React独学の特徴を理解しておきましょう。

メリット

独学には以下のような利点があります。

  • 費用が安い: 書籍やオンライン教材のみで学習可能
  • 自分のペースで学習: 時間の自由度が高い
  • 実践重視: 自分で考えて問題を解決する力が身につく

デメリット

一方で、こんな課題もあります。

  • 質問できる相手がいない: 困ったときのサポート不足
  • 体系的でない: 学習順序を自分で決める必要
  • モチベーション維持: 一人で続ける難しさ

でも大丈夫です! これらの課題は、適切な学習方法で解決できます。

これらを踏まえて、効率的な学習ステップを見ていきましょう。

ステップ1:環境構築と基本セットアップ

まず、React開発に必要な環境を整えましょう。

開発環境の準備

React開発に必要なツールをインストールします。

Node.jsとnpmの確認

# バージョン確認
node --version  # v18以上推奨
npm --version   # v8以上推奨

# 最新版でない場合は公式サイトからダウンロード
# https://nodejs.org/

このコマンドでバージョンを確認できます。

Node.jsはv18以上、npmはv8以上を使うことをおすすめします。 古いバージョンだと、新しいReactの機能が使えない場合があります。

エディタの準備

Visual Studio Codeに以下の拡張機能をインストールしましょう。

  • ES7+ React/Redux/React-Native snippets: Reactのコード補完
  • Prettier: コードフォーマッター
  • ESLint: コードの品質チェック
  • Auto Rename Tag: タグの自動リネーム
  • Bracket Pair Colorizer: 括弧の色分け

これらの拡張機能があると、開発効率が大幅に向上します。

最初のReactプロジェクト作成

# Create React Appを使用した方法
npx create-react-app my-first-react
cd my-first-react
npm start

# Viteを使用した方法(推奨・高速)
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev

この2つの方法でReactプロジェクトを作成できます。

Create React Appは公式ツールで安定しています。 Viteは新しいツールですが、起動が高速でおすすめです。

どちらでも構いませんが、初心者の方はCreate React Appから始めると良いでしょう。

プロジェクト構造の理解

my-react-app/
├── public/
│   └── index.html        # メインHTMLファイル
├── src/
│   ├── App.js           # メインコンポーネント
│   ├── App.css          # アプリのスタイル
│   ├── index.js         # エントリーポイント
│   └── index.css        # グローバルスタイル
├── package.json         # 依存関係とスクリプト
└── README.md           # プロジェクト説明

Reactプロジェクトの基本的な構造です。

srcフォルダの中に、React関連のファイルが入っています。 App.jsがメインのコンポーネントで、ここから開発を始めます。

基本的なReactコンポーネント作成

// src/App.js - 最初のコンポーネント
function App() {
  return (
    <div className="App">
      <header className="App-header">
        <h1>React学習開始!</h1>
        <p>独学でReactをマスターしよう</p>
      </header>
    </div>
  );
}

export default App;

これが最初のReactコンポーネントです。

function App()で関数コンポーネントを定義しています。 returnの中に、HTMLのようなコード(JSX)を書きます。

classNameはHTMLのclassと同じ意味です。 ReactではclassではなくclassNameを使うので注意しましょう。

/* src/App.css - スタイリング */
.App {
  text-align: center;
}

.App-header {
  background-color: #282c34;
  padding: 20px;
  color: white;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

このCSSでアプリのスタイルを設定します。

Flexboxを使って、コンテンツを画面の中央に配置しています。 min-height: 100vhで画面全体の高さを確保しています。

このステップで、Reactの開発環境が整い、最初のアプリケーションが動作することを確認できます。

ステップ2:JSXとコンポーネントの理解

Reactの核となるJSXとコンポーネントについて学習しましょう。

JSXの基本ルール

JSXはJavaScriptの拡張構文で、HTMLライクな記述ができます。

JSXの書き方

// JSXの基本的な書き方
function Welcome() {
  const name = "太郎";
  const isLoggedIn = true;
  const hobbies = ["読書", "映画鑑賞", "プログラミング"];
  
  return (
    <div className="welcome">
      {/* JSX内でのコメント */}
      <h1>こんにちは、{name}さん!</h1>
      
      {/* 条件レンダリング */}
      {isLoggedIn ? (
        <p>ログイン済みです</p>
      ) : (
        <p>ログインしてください</p>
      )}
      
      {/* リストレンダリング */}
      <h2>趣味一覧</h2>
      <ul>
        {hobbies.map((hobby, index) => (
          <li key={index}>{hobby}</li>
        ))}
      </ul>
      
      {/* イベントハンドリング */}
      <button onClick={() => alert('ボタンが押されました!')}>
        クリック
      </button>
    </div>
  );
}

このコードでJSXの基本的な書き方を理解できます。

{}の中にJavaScriptの式を書けます。 変数の値を表示したり、計算結果を表示したりできます。

条件レンダリングでは、? :(三項演算子)を使って条件によって表示を切り替えます。 リストレンダリングでは、mapメソッドで配列の要素を一つずつ表示します。

イベントハンドリングでは、onClickでクリック時の処理を定義します。

JSXの重要なルール

// ❌ 間違った書き方
function BadExample() {
  return (
    <h1>タイトル</h1>
    <p>説明文</p>  // エラー:複数の要素を返せない
  );
}

// ✅ 正しい書き方1:親要素で囲む
function GoodExample1() {
  return (
    <div>
      <h1>タイトル</h1>
      <p>説明文</p>
    </div>
  );
}

// ✅ 正しい書き方2:React.Fragmentを使用
function GoodExample2() {
  return (
    <>
      <h1>タイトル</h1>
      <p>説明文</p>
    </>
  );
}

// ✅ className(classではない)
function StyledComponent() {
  return (
    <div className="container">  {/* class ではなく className */}
      <label htmlFor="email">メール</label>  {/* for ではなく htmlFor */}
      <input type="email" id="email" />
    </div>
  );
}

JSXには重要なルールがあります。

複数の要素を返すときは、必ず親要素で囲む必要があります。 親要素を作りたくない場合は、<>...</>(React.Fragment)を使います。

また、HTMLの属性名が少し違います。 classclassNameforhtmlForを使います。

これは、JavaScriptの予約語と重複するのを避けるためです。

関数コンポーネントの作成

// 基本的な関数コンポーネント
function UserCard({ user }) {
  return (
    <div className="user-card">
      <img src={user.avatar} alt={user.name} />
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <p>年齢: {user.age}歳</p>
    </div>
  );
}

// デフォルトProps
function Button({ children, type = "button", onClick }) {
  return (
    <button type={type} onClick={onClick} className="btn">
      {children}
    </button>
  );
}

// 複数のコンポーネントを組み合わせ
function UserList() {
  const users = [
    { id: 1, name: "太郎", email: "taro@example.com", age: 25, avatar: "/avatar1.jpg" },
    { id: 2, name: "花子", email: "hanako@example.com", age: 30, avatar: "/avatar2.jpg" },
    { id: 3, name: "次郎", email: "jiro@example.com", age: 22, avatar: "/avatar3.jpg" }
  ];

  const handleUserClick = (user) => {
    alert(`${user.name}がクリックされました`);
  };

  return (
    <div className="user-list">
      <h2>ユーザー一覧</h2>
      <div className="users-grid">
        {users.map(user => (
          <div key={user.id} onClick={() => handleUserClick(user)}>
            <UserCard user={user} />
          </div>
        ))}
      </div>
      
      <Button onClick={() => alert('新規ユーザー追加')}>
        新規ユーザー追加
      </Button>
    </div>
  );
}

コンポーネントの作成方法を詳しく見てみましょう。

UserCardは、ユーザー情報を表示するコンポーネントです。 {user}でprops(プロパティ)として親から受け取った値を使います。

Buttonは、再利用可能なボタンコンポーネントです。 type = "button"でデフォルト値を設定できます。 childrenは、タグの中身(子要素)を表します。

UserListは、複数のコンポーネントを組み合わせたコンポーネントです。 ユーザーのリストを表示し、クリック時の処理も定義しています。

Propsによるデータ受け渡し

// 親コンポーネント
function App() {
  const blogPost = {
    title: "React独学のススメ",
    content: "Reactは独学でも十分習得可能です...",
    author: "プログラマー太郎",
    publishedDate: "2024-01-15",
    tags: ["React", "独学", "プログラミング"]
  };

  return (
    <div className="App">
      <Header siteTitle="技術ブログ" />
      <BlogPost post={blogPost} />
      <Footer />
    </div>
  );
}

// 子コンポーネント
function Header({ siteTitle }) {
  return (
    <header className="header">
      <h1>{siteTitle}</h1>
      <nav>
        <a href="/">ホーム</a>
        <a href="/about">会社概要</a>
        <a href="/contact">お問い合わせ</a>
      </nav>
    </header>
  );
}

function BlogPost({ post }) {
  return (
    <article className="blog-post">
      <h2>{post.title}</h2>
      <div className="meta">
        <span>著者: {post.author}</span>
        <span>公開日: {post.publishedDate}</span>
      </div>
      
      <div className="content">
        <p>{post.content}</p>
      </div>
      
      <div className="tags">
        {post.tags.map(tag => (
          <span key={tag} className="tag">#{tag}</span>
        ))}
      </div>
    </article>
  );
}

function Footer() {
  return (
    <footer className="footer">
      <p>&copy; 2024 技術ブログ. All rights reserved.</p>
    </footer>
  );
}

Propsを使ったデータの受け渡し方法です。

親コンポーネント(App)から子コンポーネント(Header、BlogPost)にデータを渡しています。

siteTitle="技術ブログ"のように、属性として値を渡します。 子コンポーネントでは{ siteTitle }として受け取ります。

オブジェクトや配列も渡せます。 post={blogPost}でオブジェクト全体を渡し、post.titleのようにプロパティにアクセスします。

この仕組みを理解すれば、コンポーネント間のデータのやり取りができるようになります。

ステップ3:State管理とHooksの習得

Reactの動的な機能を実現するStateとHooksを学習しましょう。

useState Hook

import { useState } from 'react';

// 基本的なuseStateの使用
function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(0);

  return (
    <div className="counter">
      <h2>カウンター: {count}</h2>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>リセット</button>
      
      <div>
        <input 
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="名前を入力"
        />
        <p>こんにちは、{name || "名無し"}さん!</p>
      </div>
    </div>
  );
}

useStateはReactの基本的なHookです。

const [count, setCount] = useState(0);でstateを定義します。 countが現在の値、setCountが値を更新する関数です。 useState(0)0が初期値になります。

ボタンをクリックするとsetCountが呼ばれ、画面が自動的に更新されます。 これがReactのリアクティブな仕組みです。

入力フィールドでは、onChangeイベントで入力値をstateに保存しています。

// オブジェクトのStateを管理
function UserForm() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  });

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setUser(prevUser => ({
      ...prevUser,
      [name]: value
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('送信されたユーザー情報:', user);
    alert(`ユーザー ${user.name} が登録されました!`);
  };

  return (
    <form onSubmit={handleSubmit} className="user-form">
      <h2>ユーザー登録</h2>
      
      <div>
        <label htmlFor="name">名前:</label>
        <input
          type="text"
          id="name"
          name="name"
          value={user.name}
          onChange={handleInputChange}
          required
        />
      </div>
      
      <div>
        <label htmlFor="email">メールアドレス:</label>
        <input
          type="email"
          id="email"
          name="email"
          value={user.email}
          onChange={handleInputChange}
          required
        />
      </div>
      
      <div>
        <label htmlFor="age">年齢:</label>
        <input
          type="number"
          id="age"
          name="age"
          value={user.age}
          onChange={handleInputChange}
          min="0"
          max="120"
        />
      </div>
      
      <button type="submit">登録</button>
      
      <div className="preview">
        <h3>プレビュー</h3>
        <p>名前: {user.name}</p>
        <p>メール: {user.email}</p>
        <p>年齢: {user.age}歳</p>
      </div>
    </form>
  );
}

オブジェクトをstateで管理する方法です。

useStateでオブジェクトを初期値として設定できます。 更新時は...prevUserスプレッド演算子を使い、既存の値をコピーしています。

[name]: value動的なプロパティ名を設定できます。 name属性と同名のプロパティが更新されます。

フォームが送信されるとhandleSubmitが実行され、ユーザー情報がコンソールに表示されます。

useEffect Hook

import { useState, useEffect } from 'react';

// 基本的なuseEffectの使用
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // マウント時とuserIdが変更された時に実行
  useEffect(() => {
    async function fetchUser() {
      try {
        setLoading(true);
        setError(null);
        
        // APIからユーザー情報を取得
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
          throw new Error('ユーザー情報の取得に失敗しました');
        }
        
        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }

    fetchUser();
  }, [userId]); // 依存配列にuserIdを指定

  if (loading) return <div className="loading">読み込み中...</div>;
  if (error) return <div className="error">エラー: {error}</div>;
  if (!user) return <div>ユーザーが見つかりません</div>;

  return (
    <div className="user-profile">
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
      <p>登録日: {new Date(user.createdAt).toLocaleDateString()}</p>
    </div>
  );
}

useEffect副作用を処理するHookです。

コンポーネントがマウント(表示)されたときや、stateが変更されたときに実行されます。 useEffectの第二引数の配列(依存配列)に指定した値が変わると、再実行されます。

この例では、userIdが変わるたびにAPIからユーザー情報を取得しています。

ローディング状態エラー状態成功状態を管理することで、ユーザーに適切なフィードバックを提供できます。

// クリーンアップ関数
useEffect(() => {
  console.log('コンポーネントがマウントされました');
  
  // タイマーの設定
  const timer = setInterval(() => {
    console.log('定期実行中...');
  }, 5000);

  // クリーンアップ(アンマウント時に実行)
  return () => {
    console.log('コンポーネントがアンマウントされます');
    clearInterval(timer);
  };
}, []); // 空の依存配列でマウント時のみ実行

// リアルタイムデータの更新
function LiveClock() {
  const [currentTime, setCurrentTime] = useState(new Date());

  useEffect(() => {
    const timer = setInterval(() => {
      setCurrentTime(new Date());
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  return (
    <div className="live-clock">
      <h2>現在時刻</h2>
      <p>{currentTime.toLocaleTimeString()}</p>
    </div>
  );
}

useEffectではクリーンアップ関数を返すことができます。

これは、コンポーネントがアンマウント(非表示)されるときに実行されます。 タイマーやイベントリスナーなどを解除するときに使います。

LiveClockコンポーネントでは、1秒ごとに時刻を更新しています。 コンポーネントが非表示になったとき、タイマーを自動的に停止します。

実践的なTodoアプリ

import { useState, useEffect } from 'react';

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [inputValue, setInputValue] = useState('');
  const [filter, setFilter] = useState('all'); // all, active, completed

  // ローカルストレージからデータを読み込み
  useEffect(() => {
    const savedTodos = localStorage.getItem('todos');
    if (savedTodos) {
      setTodos(JSON.parse(savedTodos));
    }
  }, []);

  // todosが変更されたらローカルストレージに保存
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  const addTodo = () => {
    if (inputValue.trim()) {
      const newTodo = {
        id: Date.now(),
        text: inputValue.trim(),
        completed: false,
        createdAt: new Date().toISOString()
      };
      setTodos([...todos, newTodo]);
      setInputValue('');
    }
  };

  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  const clearCompleted = () => {
    setTodos(todos.filter(todo => !todo.completed));
  };

  // フィルタリング
  const filteredTodos = todos.filter(todo => {
    switch (filter) {
      case 'active':
        return !todo.completed;
      case 'completed':
        return todo.completed;
      default:
        return true;
    }
  });

  const completedCount = todos.filter(todo => todo.completed).length;
  const activeCount = todos.length - completedCount;

  return (
    <div className="todo-app">
      <h1>Todoアプリ</h1>
      
      {/* 新規Todo追加 */}
      <div className="todo-input">
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
          placeholder="新しいタスクを入力..."
        />
        <button onClick={addTodo}>追加</button>
      </div>

      {/* フィルター */}
      <div className="filters">
        <button 
          className={filter === 'all' ? 'active' : ''}
          onClick={() => setFilter('all')}
        >
          すべて ({todos.length})
        </button>
        <button 
          className={filter === 'active' ? 'active' : ''}
          onClick={() => setFilter('active')}
        >
          未完了 ({activeCount})
        </button>
        <button 
          className={filter === 'completed' ? 'active' : ''}
          onClick={() => setFilter('completed')}
        >
          完了済み ({completedCount})
        </button>
      </div>

      {/* Todoリスト */}
      <ul className="todo-list">
        {filteredTodos.map(todo => (
          <li key={todo.id} className={`todo-item ${todo.completed ? 'completed' : ''}`}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span className="todo-text">{todo.text}</span>
            <button onClick={() => deleteTodo(todo.id)} className="delete-btn">
              削除
            </button>
          </li>
        ))}
      </ul>

      {/* アクション */}
      <div className="actions">
        {completedCount > 0 && (
          <button onClick={clearCompleted}>
            完了済みを削除 ({completedCount}件)
          </button>
        )}
      </div>
    </div>
  );
}

実践的なTodoアプリを作成してみました。

このアプリでは、以下の機能を実装しています:

  • Todo の追加・削除・完了切り替え
  • フィルタリング(すべて・未完了・完了済み)
  • ローカルストレージへの保存

2つのuseEffectを使っています。 1つ目でローカルストレージからデータを読み込み、2つ目でデータを保存しています。

フィルタリングでは、filterstateの値に応じて表示するTodoを変更しています。 switch文を使って、条件に応じた配列を返しています。

このTodoアプリで、useStateuseEffectの実践的な使い方を理解できます。

ステップ4:イベント処理とフォーム

ユーザーとの相互作用を実装する方法を学習しましょう。

イベントハンドリングの基本

import { useState } from 'react';

function EventExamples() {
  const [message, setMessage] = useState('');
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [keyPressed, setKeyPressed] = useState('');

  // クリックイベント
  const handleClick = (e) => {
    console.log('クリックされました', e.target);
    setMessage(`ボタンがクリックされました: ${new Date().toLocaleTimeString()}`);
  };

  // マウス移動イベント
  const handleMouseMove = (e) => {
    setPosition({ x: e.clientX, y: e.clientY });
  };

  // キーボードイベント
  const handleKeyDown = (e) => {
    setKeyPressed(`押されたキー: ${e.key}`);
    
    // 特定のキーでアクション
    if (e.key === 'Enter') {
      setMessage('Enterキーが押されました!');
    }
  };

  // ダブルクリックイベント
  const handleDoubleClick = () => {
    setMessage('ダブルクリックされました!');
  };

  return (
    <div className="event-examples">
      <h2>イベント処理の例</h2>
      
      <div className="section">
        <h3>クリックイベント</h3>
        <button onClick={handleClick}>クリック</button>
        <button onDoubleClick={handleDoubleClick}>ダブルクリック</button>
        <p>{message}</p>
      </div>

      <div className="section">
        <h3>マウス位置</h3>
        <div 
          className="mouse-area"
          onMouseMove={handleMouseMove}
          style={{ 
            width: '300px', 
            height: '200px', 
            border: '1px solid #ccc',
            padding: '10px'
          }}
        >
          マウスを動かしてください
          <p>X: {position.x}, Y: {position.y}</p>
        </div>
      </div>

      <div className="section">
        <h3>キーボードイベント</h3>
        <input 
          type="text"
          onKeyDown={handleKeyDown}
          placeholder="何か入力してください"
        />
        <p>{keyPressed}</p>
      </div>
    </div>
  );
}

Reactでのイベント処理方法を学びましょう。

クリックイベントでは、onClickでボタンクリック時の処理を定義します。 e.targetでクリックされた要素の情報を取得できます。

マウス移動イベントでは、onMouseMoveでマウスの座標を取得しています。 e.clientXe.clientYで画面上の座標が分かります。

キーボードイベントでは、onKeyDownで押されたキーを検知します。 e.keyで具体的なキー名を取得できます。

イベントハンドラー関数では、eというパラメータでイベント情報を受け取ります。

複雑なフォーム処理

import { useState } from 'react';

function AdvancedForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: '',
    confirmPassword: '',
    age: '',
    gender: '',
    hobbies: [],
    newsletter: false,
    comments: ''
  });

  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  // 入力値の変更処理
  const handleInputChange = (e) => {
    const { name, value, type, checked } = e.target;
    
    if (type === 'checkbox') {
      if (name === 'hobbies') {
        // 複数選択チェックボックス
        setFormData(prev => ({
          ...prev,
          hobbies: checked 
            ? [...prev.hobbies, value]
            : prev.hobbies.filter(hobby => hobby !== value)
        }));
      } else {
        // 単一チェックボックス
        setFormData(prev => ({
          ...prev,
          [name]: checked
        }));
      }
    } else {
      setFormData(prev => ({
        ...prev,
        [name]: value
      }));
    }

    // エラーをクリア
    if (errors[name]) {
      setErrors(prev => ({
        ...prev,
        [name]: ''
      }));
    }
  };

  // バリデーション
  const validateForm = () => {
    const newErrors = {};

    if (!formData.name.trim()) {
      newErrors.name = '名前は必須です';
    }

    if (!formData.email.trim()) {
      newErrors.email = 'メールアドレスは必須です';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = '有効なメールアドレスを入力してください';
    }

    if (!formData.password) {
      newErrors.password = 'パスワードは必須です';
    } else if (formData.password.length < 8) {
      newErrors.password = 'パスワードは8文字以上で入力してください';
    }

    if (formData.password !== formData.confirmPassword) {
      newErrors.confirmPassword = 'パスワードが一致しません';
    }

    if (!formData.age || formData.age < 0 || formData.age > 120) {
      newErrors.age = '有効な年齢を入力してください';
    }

    if (!formData.gender) {
      newErrors.gender = '性別を選択してください';
    }

    return newErrors;
  };

  // フォーム送信
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    const newErrors = validateForm();
    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors);
      return;
    }

    setIsSubmitting(true);
    try {
      // API送信のシミュレーション
      await new Promise(resolve => setTimeout(resolve, 2000));
      
      console.log('送信データ:', formData);
      alert('登録が完了しました!');
      
      // フォームをリセット
      setFormData({
        name: '',
        email: '',
        password: '',
        confirmPassword: '',
        age: '',
        gender: '',
        hobbies: [],
        newsletter: false,
        comments: ''
      });
    } catch (error) {
      alert('エラーが発生しました。もう一度お試しください。');
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="advanced-form">
      <h2>ユーザー登録フォーム</h2>

      {/* 名前 */}
      <div className="form-group">
        <label htmlFor="name">名前 *</label>
        <input
          type="text"
          id="name"
          name="name"
          value={formData.name}
          onChange={handleInputChange}
          className={errors.name ? 'error' : ''}
        />
        {errors.name && <span className="error-message">{errors.name}</span>}
      </div>

      {/* メールアドレス */}
      <div className="form-group">
        <label htmlFor="email">メールアドレス *</label>
        <input
          type="email"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleInputChange}
          className={errors.email ? 'error' : ''}
        />
        {errors.email && <span className="error-message">{errors.email}</span>}
      </div>

      {/* 性別 */}
      <div className="form-group">
        <label>性別 *</label>
        <div className="radio-group">
          <label>
            <input
              type="radio"
              name="gender"
              value="male"
              checked={formData.gender === 'male'}
              onChange={handleInputChange}
            />
            男性
          </label>
          <label>
            <input
              type="radio"
              name="gender"
              value="female"
              checked={formData.gender === 'female'}
              onChange={handleInputChange}
            />
            女性
          </label>
        </div>
        {errors.gender && <span className="error-message">{errors.gender}</span>}
      </div>

      {/* 趣味 */}
      <div className="form-group">
        <label>趣味</label>
        <div className="checkbox-group">
          {['読書', '映画鑑賞', 'スポーツ', '音楽', 'プログラミング'].map(hobby => (
            <label key={hobby}>
              <input
                type="checkbox"
                name="hobbies"
                value={hobby}
                checked={formData.hobbies.includes(hobby)}
                onChange={handleInputChange}
              />
              {hobby}
            </label>
          ))}
        </div>
      </div>

      <button type="submit" disabled={isSubmitting} className="submit-button">
        {isSubmitting ? '送信中...' : '登録'}
      </button>
    </form>
  );
}

複雑なフォーム処理の実装方法です。

入力値の変更処理では、入力の種類によって処理を分けています。 チェックボックスの場合はcheckedの値、その他はvalueの値を使います。

複数選択チェックボックスでは、配列に追加・削除を行います。 checkedがtrueなら配列に追加、falseなら配列から削除します。

バリデーションでは、各項目の入力チェックを行います。 メールアドレスの形式チェックには正規表現を使用しています。

フォーム送信では、バリデーションを実行してからAPIに送信します。 送信中はisSubmittingでボタンを無効化し、ユーザビリティを向上させています。

このフォームで、実際のWebアプリケーションで使われるフォーム処理を学べます。

ステップ5:API連携とデータ取得

実際のWebアプリケーションでは、サーバーとのデータのやり取りが必要です。

基本的なAPI連携

import { useState, useEffect } from 'react';

// カスタムフック:API呼び出し
function useApi(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchData() {
      try {
        setLoading(true);
        setError(null);
        
        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.message);
      } finally {
        setLoading(false);
      }
    }

    fetchData();
  }, [url]);

  return { data, loading, error };
}

カスタムフックを作成して、API呼び出しを再利用できるようにしました。

useApiは、URLを受け取ってAPIからデータを取得します。 ローディング状態エラー状態データを管理して返します。

fetchでAPIを呼び出し、response.okでエラーチェックを行います。 エラーが発生した場合は、適切なメッセージを設定します。

// ユーザー一覧を表示するコンポーネント
function UserListWithAPI() {
  const { data: users, loading, error } = useApi('/api/users');
  const [selectedUser, setSelectedUser] = useState(null);

  if (loading) return <div className="loading">読み込み中...</div>;
  if (error) return <div className="error">エラー: {error}</div>;

  return (
    <div className="user-list-api">
      <h2>ユーザー一覧</h2>
      
      <div className="users-grid">
        {users.map(user => (
          <div 
            key={user.id} 
            className={`user-card ${selectedUser?.id === user.id ? 'selected' : ''}`}
            onClick={() => setSelectedUser(user)}
          >
            <img src={user.avatar} alt={user.name} />
            <h3>{user.name}</h3>
            <p>{user.email}</p>
          </div>
        ))}
      </div>

      {selectedUser && (
        <UserDetail user={selectedUser} onClose={() => setSelectedUser(null)} />
      )}
    </div>
  );
}

// ユーザー詳細モーダル
function UserDetail({ user, onClose }) {
  const [posts, setPosts] = useState([]);
  const [postsLoading, setPostsLoading] = useState(true);

  useEffect(() => {
    async function fetchUserPosts() {
      try {
        setPostsLoading(true);
        const response = await fetch(`/api/users/${user.id}/posts`);
        const userPosts = await response.json();
        setPosts(userPosts);
      } catch (error) {
        console.error('投稿の取得に失敗:', error);
      } finally {
        setPostsLoading(false);
      }
    }

    fetchUserPosts();
  }, [user.id]);

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <button className="close-button" onClick={onClose}>×</button>
        
        <div className="user-detail">
          <img src={user.avatar} alt={user.name} />
          <h2>{user.name}</h2>
          <p>Email: {user.email}</p>
          <p>電話: {user.phone}</p>
          <p>ウェブサイト: {user.website}</p>
          
          <h3>投稿一覧</h3>
          {postsLoading ? (
            <p>投稿を読み込み中...</p>
          ) : (
            <ul className="posts-list">
              {posts.map(post => (
                <li key={post.id}>
                  <h4>{post.title}</h4>
                  <p>{post.body.substring(0, 100)}...</p>
                </li>
              ))}
            </ul>
          )}
        </div>
      </div>
    </div>
  );
}

実際のAPIからデータを取得して表示するコンポーネントです。

UserListWithAPIでは、カスタムフックuseApiを使ってユーザー一覧を取得しています。 ユーザーカードをクリックすると、選択状態になり詳細モーダルが表示されます。

UserDetailでは、選択されたユーザーの投稿一覧を別途取得しています。 useEffectでユーザーIDが変わるたびに投稿を取得し直します。

モーダルでは、背景クリックで閉じる機能を実装しています。 e.stopPropagation()で、モーダル内容クリック時の閉じる動作を防いでいます。

データ操作(CRUD)

import { useState, useEffect } from 'react';

function TodoManager() {
  const [todos, setTodos] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [newTodo, setNewTodo] = useState('');
  const [editingId, setEditingId] = useState(null);
  const [editText, setEditText] = useState('');

  // データ取得(READ)
  useEffect(() => {
    fetchTodos();
  }, []);

  const fetchTodos = async () => {
    try {
      setLoading(true);
      const response = await fetch('/api/todos');
      if (!response.ok) throw new Error('取得に失敗しました');
      
      const data = await response.json();
      setTodos(data);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  // 新規作成(CREATE)
  const createTodo = async () => {
    if (!newTodo.trim()) return;

    try {
      const response = await fetch('/api/todos', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          text: newTodo.trim(),
          completed: false
        }),
      });

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

      const createdTodo = await response.json();
      setTodos(prev => [...prev, createdTodo]);
      setNewTodo('');
    } catch (err) {
      alert(`エラー: ${err.message}`);
    }
  };

  // 更新(UPDATE)
  const updateTodo = async (id, updates) => {
    try {
      const response = await fetch(`/api/todos/${id}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(updates),
      });

      if (!response.ok) throw new Error('更新に失敗しました');

      const updatedTodo = await response.json();
      setTodos(prev => prev.map(todo => 
        todo.id === id ? updatedTodo : todo
      ));
    } catch (err) {
      alert(`エラー: ${err.message}`);
    }
  };

  // 削除(DELETE)
  const deleteTodo = async (id) => {
    if (!window.confirm('本当に削除しますか?')) return;

    try {
      const response = await fetch(`/api/todos/${id}`, {
        method: 'DELETE',
      });

      if (!response.ok) throw new Error('削除に失敗しました');

      setTodos(prev => prev.filter(todo => todo.id !== id));
    } catch (err) {
      alert(`エラー: ${err.message}`);
    }
  };

  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;

  return (
    <div className="todo-manager">
      <h2>Todo管理(API連携)</h2>

      {/* 新規Todo追加 */}
      <div className="add-todo">
        <input
          type="text"
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && createTodo()}
          placeholder="新しいタスクを入力..."
        />
        <button onClick={createTodo}>追加</button>
      </div>

      {/* Todoリスト */}
      <ul className="todo-list">
        {todos.map(todo => (
          <li key={todo.id} className={`todo-item ${todo.completed ? 'completed' : ''}`}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={(e) => updateTodo(todo.id, { completed: e.target.checked })}
            />

            {editingId === todo.id ? (
              <div className="edit-mode">
                <input
                  type="text"
                  value={editText}
                  onChange={(e) => setEditText(e.target.value)}
                  onKeyPress={(e) => {
                    if (e.key === 'Enter') finishEdit();
                    if (e.key === 'Escape') cancelEdit();
                  }}
                  autoFocus
                />
                <button onClick={finishEdit}>保存</button>
                <button onClick={cancelEdit}>キャンセル</button>
              </div>
            ) : (
              <div className="view-mode">
                <span className="todo-text">{todo.text}</span>
                <div className="actions">
                  <button onClick={() => startEdit(todo)}>編集</button>
                  <button onClick={() => deleteTodo(todo.id)}>削除</button>
                </div>
              </div>
            )}
          </li>
        ))}
      </ul>

      <div className="summary">
        <p>全{todos.length}件 | 完了: {todos.filter(t => t.completed).length}件</p>
      </div>
    </div>
  );
}

CRUD操作(Create, Read, Update, Delete)を実装したTodoマネージャーです。

**CREATE(新規作成)**では、POSTメソッドでデータを送信します。 Content-Type: application/jsonヘッダーと、JSONデータを送信します。

**READ(読み取り)**では、GETメソッドでデータを取得します。 コンポーネントのマウント時に実行されます。

**UPDATE(更新)**では、PUTメソッドで既存データを更新します。 チェックボックスの状態変更や、テキストの編集で使用します。

**DELETE(削除)**では、DELETEメソッドでデータを削除します。 削除前に確認ダイアログを表示して、誤操作を防ぎます。

各操作でエラーハンドリングを行い、ユーザーに適切なフィードバックを提供しています。

この実装で、実際のWebアプリケーションで必要なAPI連携のパターンを学べます。

ステップ6:ルーティングとSPA開発

単一ページアプリケーション(SPA)の基本となるルーティングを学習しましょう。

React Router の基本

# React Routerをインストール
npm install react-router-dom

まず、React Routerをインストールします。

これは、Reactでページ遷移を実現するための標準的なライブラリです。

import { BrowserRouter as Router, Routes, Route, Link, useNavigate, useParams } from 'react-router-dom';

// メインアプリケーション
function App() {
  return (
    <Router>
      <div className="app">
        <Header />
        <main>
          <Routes>
            <Route path="/" element={<HomePage />} />
            <Route path="/about" element={<AboutPage />} />
            <Route path="/products" element={<ProductsPage />} />
            <Route path="/products/:id" element={<ProductDetailPage />} />
            <Route path="/contact" element={<ContactPage />} />
            <Route path="/user/:userId/profile" element={<UserProfilePage />} />
            <Route path="*" element={<NotFoundPage />} />
          </Routes>
        </main>
        <Footer />
      </div>
    </Router>
  );
}

React Routerの基本的な設定です。

Routerで全体を囲み、Routesの中にRouteを定義します。 pathでURLパス、elementで表示するコンポーネントを指定します。

動的ルートでは、:idのようにパラメータを指定できます。 path="*"は、どのルートにもマッチしない場合の404ページです。

// ヘッダーコンポーネント(ナビゲーション)
function Header() {
  return (
    <header className="header">
      <div className="container">
        <h1>
          <Link to="/">ECサイト</Link>
        </h1>
        
        <nav className="navigation">
          <Link to="/" className="nav-link">ホーム</Link>
          <Link to="/products" className="nav-link">商品一覧</Link>
          <Link to="/about" className="nav-link">会社概要</Link>
          <Link to="/contact" className="nav-link">お問い合わせ</Link>
        </nav>
      </div>
    </header>
  );
}

// ホームページ
function HomePage() {
  const navigate = useNavigate();

  const handleShopNow = () => {
    navigate('/products');
  };

  return (
    <div className="home-page">
      <section className="hero">
        <h2>素晴らしい商品をお届けします</h2>
        <p>最新のトレンドアイテムを取り揃えています</p>
        <button onClick={handleShopNow} className="cta-button">
          ショッピングを始める
        </button>
      </section>

      <section className="featured-products">
        <h3>おすすめ商品</h3>
        <div className="products-grid">
          {[1, 2, 3].map(id => (
            <div key={id} className="product-card">
              <img src={`/images/product${id}.jpg`} alt={`商品${id}`} />
              <h4>商品{id}</h4>
              <p>¥{1000 * id}</p>
              <Link to={`/products/${id}`} className="view-detail">
                詳細を見る
              </Link>
            </div>
          ))}
        </div>
      </section>
    </div>
  );
}

ナビゲーションとホームページの実装です。

Linkコンポーネントでページ遷移を行います。 通常の<a>タグではなく、Linkを使うことでSPAの仕組みを活用できます。

useNavigateは、プログラム的にページ遷移を行うHookです。 ボタンクリック時などに、JavaScriptでページを変更できます。

おすすめ商品では、/products/${id}の形で動的なリンクを生成しています。

// 商品一覧ページ
function ProductsPage() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [category, setCategory] = useState('all');

  useEffect(() => {
    fetchProducts();
  }, [category]);

  const fetchProducts = async () => {
    try {
      setLoading(true);
      const url = category === 'all' 
        ? '/api/products' 
        : `/api/products?category=${category}`;
      
      const response = await fetch(url);
      const data = await response.json();
      setProducts(data);
    } catch (error) {
      console.error('商品取得エラー:', error);
    } finally {
      setLoading(false);
    }
  };

  if (loading) return <div>読み込み中...</div>;

  return (
    <div className="products-page">
      <h2>商品一覧</h2>
      
      <div className="filters">
        <select 
          value={category} 
          onChange={(e) => setCategory(e.target.value)}
        >
          <option value="all">すべてのカテゴリ</option>
          <option value="electronics">電子機器</option>
          <option value="clothing">衣類</option>
          <option value="books">書籍</option>
        </select>
      </div>

      <div className="products-grid">
        {products.map(product => (
          <div key={product.id} className="product-card">
            <Link to={`/products/${product.id}`}>
              <img src={product.image} alt={product.name} />
              <h3>{product.name}</h3>
              <p className="price">¥{product.price.toLocaleString()}</p>
              <p className="description">{product.description}</p>
            </Link>
          </div>
        ))}
      </div>
    </div>
  );
}

商品一覧ページでは、カテゴリフィルタ機能を実装しています。

useEffectでカテゴリが変更されるたびにAPIを呼び出し、商品一覧を取得し直します。 URLにクエリパラメータを追加して、サーバー側でフィルタリングしています。

各商品カードはLinkでラップされており、クリックすると商品詳細ページに遷移します。

// 商品詳細ページ
function ProductDetailPage() {
  const { id } = useParams();
  const navigate = useNavigate();
  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);
  const [quantity, setQuantity] = useState(1);

  useEffect(() => {
    fetchProduct();
  }, [id]);

  const fetchProduct = async () => {
    try {
      setLoading(true);
      const response = await fetch(`/api/products/${id}`);
      
      if (!response.ok) {
        throw new Error('商品が見つかりません');
      }
      
      const data = await response.json();
      setProduct(data);
    } catch (error) {
      console.error('商品詳細取得エラー:', error);
      navigate('/products'); // 商品一覧に戻る
    } finally {
      setLoading(false);
    }
  };

  const addToCart = () => {
    console.log(`商品ID ${id} を ${quantity}個 カートに追加`);
    alert(`${product.name} を ${quantity}個 カートに追加しました!`);
  };

  if (loading) return <div>読み込み中...</div>;
  if (!product) return <div>商品が見つかりません</div>;

  return (
    <div className="product-detail-page">
      <button onClick={() => navigate('/products')} className="back-button">
        ← 商品一覧に戻る
      </button>

      <div className="product-detail">
        <div className="product-images">
          <img src={product.image} alt={product.name} />
        </div>

        <div className="product-info">
          <h1>{product.name}</h1>
          <p className="price">¥{product.price.toLocaleString()}</p>
          <p className="description">{product.description}</p>

          <div className="purchase-section">
            <div className="quantity-selector">
              <label htmlFor="quantity">数量:</label>
              <select 
                id="quantity"
                value={quantity} 
                onChange={(e) => setQuantity(Number(e.target.value))}
              >
                {[...Array(10)].map((_, i) => (
                  <option key={i + 1} value={i + 1}>{i + 1}</option>
                ))}
              </select>
            </div>

            <button onClick={addToCart} className="add-to-cart">
              カートに追加
            </button>
          </div>

          <div className="product-specs">
            <h3>商品仕様</h3>
            <ul>
              <li>カテゴリ: {product.category}</li>
              <li>ブランド: {product.brand}</li>
              <li>在庫: {product.stock}個</li>
            </ul>
          </div>
        </div>
      </div>
    </div>
  );
}

商品詳細ページでは、useParamsを使ってURLパラメータを取得しています。

const { id } = useParams();で、:idの部分の値を取得できます。 このIDを使って、APIから商品詳細情報を取得します。

商品が見つからない場合は、navigate('/products')で商品一覧ページに戻します。 エラーハンドリングとユーザビリティを両立させています。

戻るボタンnavigateを使って実装しており、ブラウザの戻るボタンと同じ動作をします。

より実践的なルーティング例

// ユーザープロフィールページ
function UserProfilePage() {
  const { userId } = useParams();
  const [user, setUser] = useState(null);
  const [activeTab, setActiveTab] = useState('profile');

  useEffect(() => {
    fetchUser();
  }, [userId]);

  const fetchUser = async () => {
    try {
      const response = await fetch(`/api/users/${userId}`);
      const userData = await response.json();
      setUser(userData);
    } catch (error) {
      console.error('ユーザー情報取得エラー:', error);
    }
  };

  if (!user) return <div>読み込み中...</div>;

  return (
    <div className="user-profile-page">
      <h2>{user.name}のプロフィール</h2>

      <div className="tabs">
        <button 
          className={activeTab === 'profile' ? 'active' : ''}
          onClick={() => setActiveTab('profile')}
        >
          基本情報
        </button>
        <button 
          className={activeTab === 'orders' ? 'active' : ''}
          onClick={() => setActiveTab('orders')}
        >
          注文履歴
        </button>
        <button 
          className={activeTab === 'reviews' ? 'active' : ''}
          onClick={() => setActiveTab('reviews')}
        >
          レビュー
        </button>
      </div>

      <div className="tab-content">
        {activeTab === 'profile' && (
          <div className="profile-info">
            <p>名前: {user.name}</p>
            <p>メール: {user.email}</p>
            <p>登録日: {user.createdAt}</p>
          </div>
        )}

        {activeTab === 'orders' && (
          <div className="orders">
            <h3>注文履歴</h3>
            {/* 注文履歴の表示 */}
          </div>
        )}

        {activeTab === 'reviews' && (
          <div className="reviews">
            <h3>レビュー一覧</h3>
            {/* レビューの表示 */}
          </div>
        )}
      </div>
    </div>
  );
}

// 404ページ
function NotFoundPage() {
  const navigate = useNavigate();

  return (
    <div className="not-found-page">
      <h2>ページが見つかりません</h2>
      <p>お探しのページは存在しないか、移動された可能性があります。</p>
      <button onClick={() => navigate('/')}>
        ホームに戻る
      </button>
    </div>
  );
}

ユーザープロフィールページでは、タブ切り替え機能を実装しています。

URLパラメータからユーザーIDを取得し、そのユーザーの情報を表示します。 activeTabstateでタブの切り替えを管理しています。

404ページは、存在しないURLにアクセスした場合に表示されます。 ユーザーにエラーの説明を表示し、ホームページに戻るボタンを提供しています。

このステップで、本格的なSPAアプリケーションの基本的なルーティングを理解できます。

ステップ7:実践プロジェクトの完成

学習した内容を統合して、完全なWebアプリケーションを作成しましょう。

総合的なプロジェクト例:ブログアプリ

import { useState, useEffect, createContext, useContext } from 'react';
import { BrowserRouter as Router, Routes, Route, Link, useParams, useNavigate } from 'react-router-dom';

// 認証コンテキスト
const AuthContext = createContext();

// 認証プロバイダー
function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // ローカルストレージから認証情報を復元
    const savedUser = localStorage.getItem('user');
    if (savedUser) {
      setUser(JSON.parse(savedUser));
    }
    setLoading(false);
  }, []);

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

      if (!response.ok) throw new Error('ログインに失敗しました');

      const userData = await response.json();
      setUser(userData);
      localStorage.setItem('user', JSON.stringify(userData));
      return true;
    } catch (error) {
      console.error('ログインエラー:', error);
      return false;
    }
  };

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

  return (
    <AuthContext.Provider value={{ user, login, logout, loading }}>
      {children}
    </AuthContext.Provider>
  );
}

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

まず、認証システムを実装しました。

AuthContextで認証状態を管理し、アプリ全体で共有します。 AuthProviderで認証に関する機能(ログイン、ログアウト)を提供します。

ログイン情報はローカルストレージに保存して、ページを再読み込みしても状態を保持します。 useAuthカスタムフックで、コンポーネントから簡単に認証情報にアクセスできます。

// メインアプリ
function App() {
  return (
    <AuthProvider>
      <Router>
        <div className="app">
          <Header />
          <main className="main-content">
            <Routes>
              <Route path="/" element={<HomePage />} />
              <Route path="/posts" element={<PostsPage />} />
              <Route path="/posts/:id" element={<PostDetailPage />} />
              <Route path="/write" element={<WritePostPage />} />
              <Route path="/login" element={<LoginPage />} />
              <Route path="/profile" element={<ProfilePage />} />
              <Route path="*" element={<NotFoundPage />} />
            </Routes>
          </main>
          <Footer />
        </div>
      </Router>
    </AuthProvider>
  );
}

// ヘッダーコンポーネント
function Header() {
  const { user, logout } = useAuth();

  return (
    <header className="header">
      <div className="container">
        <h1>
          <Link to="/">Tech Blog</Link>
        </h1>

        <nav className="navigation">
          <Link to="/">ホーム</Link>
          <Link to="/posts">記事一覧</Link>
          
          {user ? (
            <>
              <Link to="/write">記事を書く</Link>
              <Link to="/profile">プロフィール</Link>
              <button onClick={logout} className="logout-btn">
                ログアウト
              </button>
              <span className="user-info">こんにちは、{user.name}さん</span>
            </>
          ) : (
            <Link to="/login">ログイン</Link>
          )}
        </nav>
      </div>
    </header>
  );
}

メインアプリの構成です。

AuthProviderで全体を囲むことで、どのコンポーネントからも認証情報にアクセスできます。 Headerでは、ログイン状態によって表示するメニューを切り替えています。

ログイン中は「記事を書く」「プロフィール」「ログアウト」ボタンを表示し、未ログインの場合は「ログイン」リンクを表示します。

// ホームページ
function HomePage() {
  const [featuredPosts, setFeaturedPosts] = useState([]);
  const [loading, setLoading] = useState(true);

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

  const fetchFeaturedPosts = async () => {
    try {
      const response = await fetch('/api/posts?featured=true&limit=6');
      const posts = await response.json();
      setFeaturedPosts(posts);
    } catch (error) {
      console.error('記事取得エラー:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="home-page">
      <section className="hero">
        <h2>最新の技術情報をお届け</h2>
        <p>プログラミングから最新技術まで、役立つ情報を発信しています</p>
        <Link to="/posts" className="cta-button">記事を読む</Link>
      </section>

      <section className="featured-posts">
        <h3>注目の記事</h3>
        {loading ? (
          <div>読み込み中...</div>
        ) : (
          <div className="posts-grid">
            {featuredPosts.map(post => (
              <article key={post.id} className="post-card">
                <img src={post.thumbnail} alt={post.title} />
                <div className="post-content">
                  <h4>
                    <Link to={`/posts/${post.id}`}>{post.title}</Link>
                  </h4>
                  <p className="excerpt">{post.excerpt}</p>
                  <div className="post-meta">
                    <span>by {post.author.name}</span>
                    <span>{new Date(post.createdAt).toLocaleDateString()}</span>
                  </div>
                </div>
              </article>
            ))}
          </div>
        )}
      </section>
    </div>
  );
}

ホームページでは、注目記事を表示しています。

APIから注目記事を6件取得し、カード形式で表示します。 各記事カードには、サムネイル、タイトル、抜粋、著者名、公開日を表示しています。

ローディング状態を管理し、データ取得中は「読み込み中...」を表示します。

// 記事一覧ページ
function PostsPage() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [searchTerm, setSearchTerm] = useState('');
  const [category, setCategory] = useState('all');
  const [currentPage, setCurrentPage] = useState(1);

  useEffect(() => {
    fetchPosts();
  }, [searchTerm, category, currentPage]);

  const fetchPosts = async () => {
    try {
      setLoading(true);
      const params = new URLSearchParams({
        page: currentPage,
        limit: 10,
        ...(searchTerm && { search: searchTerm }),
        ...(category !== 'all' && { category })
      });

      const response = await fetch(`/api/posts?${params}`);
      const data = await response.json();
      setPosts(data.posts);
    } catch (error) {
      console.error('記事取得エラー:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="posts-page">
      <h2>記事一覧</h2>

      {/* 検索・フィルター */}
      <div className="filters">
        <input
          type="text"
          placeholder="記事を検索..."
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          className="search-input"
        />

        <select 
          value={category} 
          onChange={(e) => setCategory(e.target.value)}
          className="category-select"
        >
          <option value="all">すべてのカテゴリ</option>
          <option value="javascript">JavaScript</option>
          <option value="react">React</option>
          <option value="nodejs">Node.js</option>
          <option value="css">CSS</option>
        </select>
      </div>

      {/* 記事リスト */}
      {loading ? (
        <div>読み込み中...</div>
      ) : (
        <>
          <div className="posts-list">
            {posts.map(post => (
              <article key={post.id} className="post-item">
                <div className="post-thumbnail">
                  <img src={post.thumbnail} alt={post.title} />
                </div>
                
                <div className="post-content">
                  <h3>
                    <Link to={`/posts/${post.id}`}>{post.title}</Link>
                  </h3>
                  <p className="excerpt">{post.excerpt}</p>
                  
                  <div className="post-meta">
                    <span className="author">by {post.author.name}</span>
                    <span className="date">
                      {new Date(post.createdAt).toLocaleDateString()}
                    </span>
                    <span className="category">{post.category}</span>
                  </div>
                  
                  <div className="post-stats">
                    <span>👀 {post.views}</span>
                    <span>❤️ {post.likes}</span>
                    <span>💬 {post.comments.length}</span>
                  </div>
                </div>
              </article>
            ))}
          </div>

          {/* ページネーション */}
          <div className="pagination">
            <button 
              disabled={currentPage === 1}
              onClick={() => setCurrentPage(prev => prev - 1)}
            >
              前のページ
            </button>
            <span>ページ {currentPage}</span>
            <button 
              onClick={() => setCurrentPage(prev => prev + 1)}
            >
              次のページ
            </button>
          </div>
        </>
      )}
    </div>
  );
}

記事一覧ページでは、検索・フィルタ・ページネーション機能を実装しています。

検索キーワード、カテゴリ、ページ番号が変更されるたびに、APIから記事一覧を取得し直します。 URLSearchParamsを使って、クエリパラメータを構築しています。

各記事には、閲覧数、いいね数、コメント数などの統計情報も表示しています。

ページネーションでは、前のページ・次のページボタンで記事を切り替えできます。

さらに高度な機能の実装

// 記事詳細ページ
function PostDetailPage() {
  const { id } = useParams();
  const { user } = useAuth();
  const [post, setPost] = useState(null);
  const [comments, setComments] = useState([]);
  const [newComment, setNewComment] = useState('');
  const [loading, setLoading] = useState(true);
  const [liked, setLiked] = useState(false);

  useEffect(() => {
    fetchPost();
    fetchComments();
  }, [id]);

  const handleLike = async () => {
    if (!user) {
      alert('ログインが必要です');
      return;
    }

    try {
      const response = await fetch(`/api/posts/${id}/like`, {
        method: liked ? 'DELETE' : 'POST',
        headers: {
          'Authorization': `Bearer ${user.token}`
        }
      });

      if (response.ok) {
        setLiked(!liked);
        setPost(prev => ({
          ...prev,
          likes: liked ? prev.likes - 1 : prev.likes + 1
        }));
      }
    } catch (error) {
      console.error('いいねエラー:', error);
    }
  };

  const submitComment = async (e) => {
    e.preventDefault();
    if (!user) {
      alert('ログインが必要です');
      return;
    }

    if (!newComment.trim()) return;

    try {
      const response = await fetch(`/api/posts/${id}/comments`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${user.token}`
        },
        body: JSON.stringify({ content: newComment })
      });

      if (response.ok) {
        const comment = await response.json();
        setComments(prev => [...prev, comment]);
        setNewComment('');
      }
    } catch (error) {
      console.error('コメント投稿エラー:', error);
    }
  };

  if (loading || !post) return <div>読み込み中...</div>;

  return (
    <div className="post-detail-page">
      <article className="post-detail">
        <header className="post-header">
          <img src={post.thumbnail} alt={post.title} className="post-image" />
          <h1>{post.title}</h1>
          
          <div className="post-meta">
            <div className="author-info">
              <img src={post.author.avatar} alt={post.author.name} />
              <div>
                <span className="author-name">{post.author.name}</span>
                <span className="post-date">
                  {new Date(post.createdAt).toLocaleDateString()}
                </span>
              </div>
            </div>
            
            <div className="post-actions">
              <button 
                onClick={handleLike}
                className={`like-button ${liked ? 'liked' : ''}`}
              >
                ❤️ {post.likes}
              </button>
              <span>👀 {post.views}</span>
            </div>
          </div>
        </header>

        <div className="post-content">
          <div dangerouslySetInnerHTML={{ __html: post.content }} />
        </div>

        <div className="post-tags">
          {post.tags.map(tag => (
            <span key={tag} className="tag">#{tag}</span>
          ))}
        </div>
      </article>

      {/* コメントセクション */}
      <section className="comments-section">
        <h3>コメント ({comments.length})</h3>

        {user && (
          <form onSubmit={submitComment} className="comment-form">
            <textarea
              value={newComment}
              onChange={(e) => setNewComment(e.target.value)}
              placeholder="コメントを入力..."
              rows="4"
            />
            <button type="submit">コメント投稿</button>
          </form>
        )}

        <div className="comments-list">
          {comments.map(comment => (
            <div key={comment.id} className="comment">
              <div className="comment-header">
                <img src={comment.author.avatar} alt={comment.author.name} />
                <div>
                  <span className="comment-author">{comment.author.name}</span>
                  <span className="comment-date">
                    {new Date(comment.createdAt).toLocaleDateString()}
                  </span>
                </div>
              </div>
              <p className="comment-content">{comment.content}</p>
            </div>
          ))}
        </div>
      </section>
    </div>
  );
}

記事詳細ページでは、いいね機能コメント機能を実装しています。

いいね機能では、ログイン状態をチェックし、APIにリクエストを送信します。 いいね状態に応じて、POSTまたはDELETEメソッドを使い分けています。

コメント機能では、ログインユーザーのみがコメントを投稿できます。 認証トークンをヘッダーに含めて、APIに送信します。

dangerouslySetInnerHTMLでHTMLコンテンツを表示していますが、実際のアプリケーションではXSS対策が必要です。

このブログアプリの実装で、本格的なWebアプリケーション開発のスキルを習得できます。

学習の継続とコミュニティ活用

React独学を成功させるためのコツをお伝えします。

学習の継続方法

日々の学習習慣

継続的な学習のために、以下の習慣を身につけましょう。

  • 毎日のコミット: GitHubに学習記録を残す
  • 進捗の可視化: 学習時間や完了項目を記録
  • 小さな目標設定: 週単位での達成可能な目標
// 学習進捗を管理するコンポーネント例
function LearningTracker() {
  const [progress, setProgress] = useState({
    totalHours: 0,
    completedTasks: [],
    currentGoal: '',
    achievements: []
  });

  const addStudyTime = (hours) => {
    setProgress(prev => ({
      ...prev,
      totalHours: prev.totalHours + hours
    }));
  };

  const completeTask = (task) => {
    setProgress(prev => ({
      ...prev,
      completedTasks: [...prev.completedTasks, {
        ...task,
        completedAt: new Date().toISOString()
      }]
    }));
  };

  return (
    <div className="learning-tracker">
      <h3>学習進捗</h3>
      <p>総学習時間: {progress.totalHours}時間</p>
      <p>完了タスク: {progress.completedTasks.length}個</p>
      
      <div className="recent-achievements">
        <h4>最近の成果</h4>
        {progress.completedTasks.slice(-5).map(task => (
          <div key={task.id}>
            {task.name} - {new Date(task.completedAt).toLocaleDateString()}
          </div>
        ))}
      </div>
    </div>
  );
}

学習進捗を可視化するコンポーネントの例です。

学習時間や完了したタスクを記録することで、モチベーションの維持につながります。 小さな成果でも記録することで、着実な成長を実感できます。

コミュニティ活用

オンラインコミュニティ

React学習を助けてくれるコミュニティを活用しましょう。

  • React Tokyo: 日本最大級のReactコミュニティ
  • Discord/Slack: リアルタイムでの質問・相談
  • Twitter: 技術情報の収集と発信
  • Qiita: 学習記録の公開

学習リソース

効果的な学習リソースを活用しましょう。

  • 公式ドキュメント: 最新で正確な情報
  • React DevTools: デバッグツール
  • CodeSandbox: オンラインエディタ
  • GitHub: オープンソースコードの学習

質問する前に、公式ドキュメントやGoogle検索で調べる習慣をつけましょう。 自分で調べる力も、プログラマーにとって重要なスキルです。

質問するときは、具体的なエラーメッセージ試したことを明記すると、適切な回答を得やすくなります。

まとめ

React独学について7つのステップで詳しく解説しました。

React独学は可能

適切な学習プランと継続的な実践により、独学でもReactを習得できます。

段階的な学習が重要

基礎から応用まで、7つのステップで着実にスキルアップできます。

実践プロジェクトが効果的

理論だけでなく、実際にアプリケーションを作ることで理解が深まります。

コミュニティの活用

一人で学習するデメリットは、コミュニティ参加で補えます。

継続学習が鍵

Reactは進化が早いため、継続的な学習が必要です。

React独学は決して簡単ではありませんが、この7つのステップを参考に学習を進めることで、確実にスキルを身につけることができます。

自分のペースで学習を続け、実践的なプロジェクトを通じてReact開発の楽しさを実感してください。

きっと、モダンなフロントエンド開発者として活躍できるはずです!

関連記事