【挫折しない】React学習の正しい順序と押さえるべきポイント

React初心者が挫折しない学習順序を詳しく解説。基礎から実践まで段階的に進める方法と、各段階で押さえるべき重要ポイントを具体的なコード例と共に紹介します。効率的なReact習得を実現しましょう。

Learning Next 運営
97 分で読めます

「React学習を始めたいけど、何から手をつければいいの?」

こんな風に悩んでいませんか?

「いきなりReduxから始めるべき?」 「Hooksは最初から覚えた方がいいの?」 「効率的な学習順序って何?」

実は、React学習で挫折してしまう人の多くは学習の順序を間違えています。 正しい順序で進めれば、React は思っているより簡単に習得できるんです。

この記事では、初心者が挫折せずにReactをマスターできる学習ロードマップを紹介します。 各段階で何を覚えるべきか、どんなコードを書けばいいかを具体的に解説しますね。

読み終わる頃には、あなたも「これなら続けられそう!」と感じているはずです。

なぜ多くの人がReact学習で挫折するの?

まずは「よくある失敗パターン」を知っておきましょう。 これを避けるだけで、成功率が格段に上がります。

こんな学習、していませんか?

多くの初心者が陥りがちな失敗パターンがあります。

失敗パターン1:いきなり難しいコードに挑戦

// ❌ 初心者がいきなり見ると混乱するコード
import React, { useState, useEffect, useContext, useReducer } from 'react';
import { Provider, useSelector, useDispatch } from 'react-redux';

const ComplexComponent = () => {
  const dispatch = useDispatch();
  const data = useSelector(state => state.data);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    // 複雑な処理がいっぱい...
  }, []);
  
  return (
    // たくさんのJSXコード...
  );
};

なぜこれがダメなの?

こんな複雑なコードを最初から理解しようとすると、確実に挫折します。 Reactの基本概念が分からないまま応用に手を出すからです。

失敗パターン2:JavaScript基礎を飛ばしてReactに進む

「早くReactを始めたい!」という気持ちは分かります。 でも、JavaScript の基礎が不十分だと、Reactの問題なのかJavaScriptの問題なのか分からなくなります。

失敗パターン3:環境構築で時間を使いすぎる

# ❌ 初心者が混乱する複雑な設定
npm install webpack babel-loader @babel/core @babel/preset-react
# ... 数十個のパッケージ設定

環境構築にハマって、肝心のReact学習に進めないパターンです。 最初は Create React App などの簡単なツールを使いましょう。

成功する人の学習パターン

一方で、React学習に成功する人には共通点があります。

成功する人の特徴

  • 基礎から段階的に積み上げる
  • 実際にコードを書きながら学ぶ
  • 小さなプロジェクトから始める
  • エラーを恐れずに試行錯誤する
  • 一つずつ理解してから次に進む
// ✅ 段階的に学んだ結果、理解できるコード

// 最初:シンプルなコンポーネント
function Welcome() {
  return <h1>Hello, World!</h1>;
}

// 次:propsを理解
function Welcome({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// その次:stateを学習
function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        +1
      </button>
    </div>
  );
}

この順序で学ぶメリット

  • 一つずつ確実に理解できる
  • エラーが出ても原因が分かりやすい
  • 自信を持って次のステップに進める

正しい学習マインドセット

React学習を成功させるための心構えを身につけましょう。

大切な考え方

  • 完璧を求めない→動くものを作ることを優先
  • エラーは友達→学習のチャンスと捉える
  • 焦らない→一度に全てを理解しようとしない
  • 手を動かす→実際にコードを書いて覚える
  • 人と交流→他の学習者と情報交換する

学習期間の目安

const learningStages = {
  初心者: {
    目標: "簡単なコンポーネントが作れる",
    期間: "1-2ヶ月",
    覚えること: ["JSX", "props", "基本的なstate"]
  },
  
  中級者: {
    目標: "実用的なアプリが作れる",
    期間: "2-3ヶ月", 
    覚えること: ["Hooks", "状態管理", "API連携"]
  },
  
  上級者: {
    目標: "本格的なアプリが作れる",
    期間: "3-6ヶ月",
    覚えること: ["パフォーマンス最適化", "テスト", "設計パターン"]
  }
};

重要なポイント

期間はあくまで目安です。 自分のペースで進めることが一番大切ですよ。

ステップ1:JavaScript基礎を固めよう

「早くReactを始めたいのに、なぜJavaScript?」 そう思うかもしれませんが、ここが一番重要です。

これだけは覚えておこう

React でよく使うJavaScript機能を重点的に学習しましょう。

アロー関数をマスターしよう

// ボタンをクリックしたときの処理によく使います
const handleClick = () => {
  console.log('ボタンがクリックされました');
};

// 従来の書き方と比較
function traditionalFunction() {
  console.log('従来の関数');
}

なぜアロー関数を覚えるの?

Reactのイベント処理で頻繁に使うからです。 書き方に慣れておくと、Reactのコードがスッと理解できるようになります。

分割代入は超重要

// オブジェクトの分割代入
const user = { name: '田中', age: 25, city: '東京' };
const { name, age } = user;

console.log(name); // "田中"
console.log(age);  // 25

// 配列の分割代入
const [first, second] = ['React', 'Vue'];
console.log(first);  // "React"
console.log(second); // "Vue"

ReactのuseStateでこう使います

// これが分割代入です
const [count, setCount] = useState(0);
//     ↑       ↑
//  現在の値  更新する関数

分割代入を理解しておくと、Reactのコードがとても読みやすくなります。

スプレッド演算子も必須

// 配列のコピーと追加
const originalArray = [1, 2, 3];
const newArray = [...originalArray, 4, 5];
console.log(newArray); // [1, 2, 3, 4, 5]

// オブジェクトのコピーと更新
const originalObject = { name: '田中', age: 25 };
const newObject = { ...originalObject, city: '東京' };
console.log(newObject); // { name: '田中', age: 25, city: '東京' }

Reactの状態更新でこう使います

// 配列に新しい要素を追加
setTodos([...todos, newTodo]);

// オブジェクトの一部を更新
setUser({ ...user, email: 'new@email.com' });

スプレッド演算子は、Reactの状態管理で絶対に必要な知識です。

テンプレートリテラルで文字列を便利に

const name = '田中';
const age = 25;

// 従来の書き方
const message1 = 'こんにちは、' + name + 'さん!あなたは' + age + '歳ですね。';

// テンプレートリテラル(こっちが便利)
const message2 = `こんにちは、${name}さん!あなたは${age}歳ですね。`;

ReactのJSXでも使えます

const UserProfile = ({ user }) => {
  return (
    <div>
      <h1>{`${user.name}さんのプロフィール`}</h1>
      <p>{`年齢: ${user.age}歳`}</p>
    </div>
  );
};

配列メソッドは絶対に覚えよう

Reactでリスト表示をするときに必須の知識です。

map()でリストを作る

const users = [
  { id: 1, name: '田中', active: true },
  { id: 2, name: '佐藤', active: false },
  { id: 3, name: '鈴木', active: true }
];

// 名前だけのリストを作成
const userNames = users.map(user => user.name);
console.log(userNames); // ['田中', '佐藤', '鈴木']

Reactのリストレンダリングでこう使います

function UserList({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

filter()で条件に合うものだけ抽出

// アクティブなユーザーだけを取得
const activeUsers = users.filter(user => user.active);
console.log(activeUsers); 
// [{ id: 1, name: '田中', active: true }, { id: 3, name: '鈴木', active: true }]

find()で特定の要素を探す

// IDが2のユーザーを探す
const targetUser = users.find(user => user.id === 2);
console.log(targetUser); // { id: 2, name: '佐藤', active: false }

reduce()で集計する

// アクティブなユーザーの数を数える
const activeCount = users.reduce((count, user) => {
  return user.active ? count + 1 : count;
}, 0);
console.log(activeCount); // 2

これらの配列メソッドを使いこなせるようになると、Reactでのデータ処理がとても楽になります。

非同期処理も理解しておこう

ReactでAPIからデータを取得するときに必要な知識です。

Promiseの基本

function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    // APIコールのシミュレーション
    setTimeout(() => {
      if (userId > 0) {
        resolve({ id: userId, name: `ユーザー${userId}` });
      } else {
        reject(new Error('無効なユーザーID'));
      }
    }, 1000);
  });
}

async/awaitの方が分かりやすい

async function getUserData(userId) {
  try {
    const userData = await fetchUserData(userId);
    console.log('ユーザーデータ取得成功:', userData);
    return userData;
  } catch (error) {
    console.error('エラー:', error.message);
    return null;
  }
}

// 使い方
async function showUserProfile(userId) {
  const user = await getUserData(userId);
  if (user) {
    console.log(`${user.name}さんのプロフィールを表示`);
  }
}

ReactのuseEffectでこう使います

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    async function loadUser() {
      const userData = await getUserData(userId);
      setUser(userData);
    }
    
    loadUser();
  }, [userId]);
  
  return user ? <div>{user.name}</div> : <div>読み込み中...</div>;
}

練習プロジェクトを作ってみよう

JavaScript の知識を確認するために、簡単なプロジェクトを作ってみましょう。

Vanilla JavaScript でToDo アプリ

class TodoApp {
  constructor() {
    this.todos = [];
    this.nextId = 1;
    this.init();
  }
  
  init() {
    // HTML要素を取得
    this.todoInput = document.getElementById('todoInput');
    this.todoList = document.getElementById('todoList');
    this.addButton = document.getElementById('addButton');
    
    // イベントを設定
    this.addButton.addEventListener('click', () => this.addTodo());
    this.todoInput.addEventListener('keypress', (e) => {
      if (e.key === 'Enter') this.addTodo();
    });
  }
  
  addTodo() {
    const text = this.todoInput.value.trim();
    if (!text) return;
    
    const todo = {
      id: this.nextId++,
      text: text,
      completed: false
    };
    
    this.todos.push(todo);
    this.todoInput.value = '';
    this.render();
  }
  
  toggleTodo(id) {
    const todo = this.todos.find(t => t.id === id);
    if (todo) {
      todo.completed = !todo.completed;
      this.render();
    }
  }
  
  deleteTodo(id) {
    this.todos = this.todos.filter(t => t.id !== id);
    this.render();
  }
  
  render() {
    this.todoList.innerHTML = '';
    
    this.todos.forEach(todo => {
      const li = document.createElement('li');
      li.className = todo.completed ? 'completed' : '';
      
      li.innerHTML = `
        <span onclick="app.toggleTodo(${todo.id})">${todo.text}</span>
        <button onclick="app.deleteTodo(${todo.id})">削除</button>
      `;
      
      this.todoList.appendChild(li);
    });
  }
}

// アプリを開始
const app = new TodoApp();

このコードで何を学べるの?

  • DOM操作の基本
  • イベントハンドリング
  • 配列とオブジェクトの操作
  • クラスと関数の使い方

このVanilla JavaScript版を理解できれば、Reactでの実装もスムーズに進められます。 React では、DOM操作が自動化されて、もっと簡単になりますからね。

ステップ2:React基礎概念をマスターしよう

JavaScript基礎が固まったら、いよいよReactの世界に入ります。 最初は環境構築から始めて、基本概念を一つずつ理解していきましょう。

環境構築は簡単に済ませよう

複雑な設定に時間をかけず、すぐにReact学習を始められる環境を作ります。

Create React App で一発セットアップ

# Node.js がインストールされているか確認
node --version
npm --version

# 新しいReactプロジェクトを作成
npx create-react-app my-first-react-app
cd my-first-react-app

# 開発サーバーを起動
npm start

たったこれだけで完了です

ブラウザが自動で開いて、Reactのロゴがくるくる回っているページが表示されます。 これが表示されたら成功です!

最初のコンポーネントを書いてみよう

// src/App.js を以下のように変更
import React from 'react';
import './App.css';

function App() {
  return (
    <div className="App">
      <h1>私の最初のReactアプリ</h1>
      <p>React学習を始めましょう!</p>
    </div>
  );
}

export default App;

ファイルを保存すると、ブラウザの表示が自動で更新されます。 この「ホットリロード」機能のおかげで、開発がとても楽になりますよ。

JSXって何?HTMLと何が違うの?

JSXは、JavaScriptの中でHTML風に書ける特別な記法です。

JSXの基本ルール

function MyComponent() {
  const userName = '田中太郎';
  const isLoggedIn = true;
  
  return (
    <div>
      {/* JSXのコメントの書き方 */}
      <h1>こんにちは、{userName}さん</h1>
      
      {/* JavaScriptの変数を{}で囲んで表示 */}
      <p>今日も頑張りましょう!</p>
      
      {/* 条件によって表示を切り替え */}
      {isLoggedIn ? (
        <p>ログイン中です</p>
      ) : (
        <p>ログインしてください</p>
      )}
      
      {/* HTMLとの違い:classNameを使う */}
      <div className="user-info">
        <p>ユーザー情報</p>
      </div>
      
      {/* 自己終了タグは必須 */}
      <img src="/profile.jpg" alt="プロフィール" />
      <br />
    </div>
  );
}

JSXのポイント

  • {userName} → JavaScriptの変数や式を表示
  • className → HTMLのclass属性の代わり
  • {/* コメント */} → JSX内でのコメント記法
  • 自己終了タグ必須 → <br />, <img /> など

よくあるエラーと解決方法

// ❌ エラー:隣接するJSX要素は一つの親でラップする必要がある
function ErrorExample() {
  return (
    <h1>タイトル</h1>
    <p>内容</p>
  );
}

// ✅ 修正方法1:divでラップ
function FixedExample1() {
  return (
    <div>
      <h1>タイトル</h1>
      <p>内容</p>
    </div>
  );
}

// ✅ 修正方法2:React Fragment(短縮記法)
function FixedExample2() {
  return (
    <>
      <h1>タイトル</h1>
      <p>内容</p>
    </>
  );
}

なぜFragmentを使うの?

不要なdiv要素を作らずに済むからです。 HTMLの構造をきれいに保てます。

コンポーネントの概念を理解しよう

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

シンプルなコンポーネントから始める

// 一番シンプルなコンポーネント
function Welcome() {
  return <h1>ようこそ!</h1>;
}

// パラメータ(props)を受け取るコンポーネント
function Greeting({ name, time }) {
  return (
    <div>
      <h2>こんにちは、{name}さん</h2>
      <p>現在の時刻: {time}</p>
    </div>
  );
}

// 複数のコンポーネントを組み合わせ
function App() {
  const currentTime = new Date().toLocaleTimeString();
  
  return (
    <div>
      <Welcome />
      <Greeting name="田中" time={currentTime} />
      <Greeting name="佐藤" time={currentTime} />
    </div>
  );
}

コンポーネントの良いところ

  • 再利用できる → 同じGreetingを何回でも使える
  • 管理しやすい → 小さな部品に分けて整理
  • テストしやすい → 一つずつ動作確認できる

コンポーネントを分割してみよう

// components/Header.jsx
function Header({ title }) {
  return (
    <header style={{ background: '#f0f0f0', padding: '1rem' }}>
      <h1>{title}</h1>
    </header>
  );
}

// components/UserCard.jsx
function UserCard({ user }) {
  return (
    <div style={{ 
      border: '1px solid #ddd', 
      padding: '1rem', 
      margin: '0.5rem' 
    }}>
      <h3>{user.name}</h3>
      <p>年齢: {user.age}歳</p>
      <p>職業: {user.job}</p>
    </div>
  );
}

// App.jsx
import Header from './components/Header';
import UserCard from './components/UserCard';

function App() {
  const users = [
    { id: 1, name: '田中太郎', age: 28, job: 'エンジニア' },
    { id: 2, name: '佐藤花子', age: 32, job: 'デザイナー' },
    { id: 3, name: '鈴木一郎', age: 25, job: 'マーケター' }
  ];
  
  return (
    <div>
      <Header title="ユーザー一覧" />
      <div>
        {users.map(user => (
          <UserCard key={user.id} user={user} />
        ))}
      </div>
    </div>
  );
}

map()でリストを表示する

{users.map(user => (
  <UserCard key={user.id} user={user} />
))}

この部分は、JavaScript のmap()メソッドを使っています。 配列の各要素に対してUserCardコンポーネントを作成しているんです。

key属性が重要な理由

key={user.id}は、Reactがどの要素が変更されたかを効率的に判断するために必要です。 ユニークな値(今回はユーザーのID)を設定しましょう。

Props(プロパティ)でデータを渡そう

Propsは、親コンポーネントから子コンポーネントにデータを渡す仕組みです。

Propsの基本的な使い方

// 親コンポーネント
function ParentComponent() {
  const bookData = {
    title: 'React入門',
    author: '山田太郎',
    price: 2800,
    isbn: '978-1234567890'
  };
  
  return (
    <div>
      <h1>書籍情報</h1>
      <BookCard 
        title={bookData.title}
        author={bookData.author}
        price={bookData.price}
        isbn={bookData.isbn}
      />
      
      {/* スプレッド演算子でまとめて渡すこともできる */}
      <BookCard {...bookData} />
    </div>
  );
}

// 子コンポーネント
function BookCard({ title, author, price, isbn }) {
  const formattedPrice = price.toLocaleString();
  
  return (
    <div className="book-card">
      <h2>{title}</h2>
      <p>著者: {author}</p>
      <p>価格: ¥{formattedPrice}</p>
      <p>ISBN: {isbn}</p>
    </div>
  );
}

分割代入でスッキリ書く

// propsオブジェクトをそのまま受け取る
function BookCard(props) {
  return (
    <div>
      <h2>{props.title}</h2>
      <p>著者: {props.author}</p>
    </div>
  );
}

// 分割代入で受け取る(推奨)
function BookCard({ title, author, price, isbn }) {
  return (
    <div>
      <h2>{title}</h2>
      <p>著者: {author}</p>
    </div>
  );
}

分割代入を使った方が、コードがスッキリして読みやすくなります。

デフォルト値も設定できる

function Button({ text, color, size, onClick }) {
  const buttonStyle = {
    backgroundColor: color || '#007bff',
    fontSize: size === 'large' ? '18px' : '14px',
    padding: size === 'large' ? '12px 24px' : '8px 16px',
    border: 'none',
    borderRadius: '4px',
    cursor: 'pointer'
  };
  
  return (
    <button style={buttonStyle} onClick={onClick}>
      {text || 'クリック'}
    </button>
  );
}

// 使用例
function App() {
  return (
    <div>
      <Button />
      <Button text="送信" color="green" size="large" />
      <Button text="キャンセル" color="red" />
    </div>
  );
}

重要なポイント

Propsのデータの流れは一方向(親から子へ)です。 子コンポーネントは、受け取ったpropsを変更することはできません。

これがReactの「単方向データフロー」という重要な概念です。 データの流れが明確なので、バグが起きにくくなります。

ステップ3:State(状態)とイベント処理を覚えよう

ここからReactの真の力を体験できます。 Stateを使うと、ユーザーの操作に応じて画面が動的に変わるアプリが作れるようになります。

useState Hookの使い方

useStateは、コンポーネント内でデータの状態を管理するための仕組みです。

一番シンプルなカウンター

import React, { useState } from 'react';

function Counter() {
  // [現在の値, 更新関数] = useState(初期値)
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>現在のカウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        +1
      </button>
      <button onClick={() => setCount(count - 1)}>
        -1
      </button>
      <button onClick={() => setCount(0)}>
        リセット
      </button>
    </div>
  );
}

何が起きているの?

  1. useState(0) → 初期値0でstateを作成
  2. count → 現在の値
  3. setCount → 値を更新する関数
  4. ボタンを押すとsetCountが呼ばれる
  5. 画面が自動で再描画される

複数のStateを管理する

function UserProfile() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [age, setAge] = useState('');
  const [isEditing, setIsEditing] = useState(false);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('送信されたデータ:', { name, email, age });
    setIsEditing(false);
  };
  
  return (
    <div>
      <h2>ユーザープロフィール</h2>
      
      {isEditing ? (
        <form onSubmit={handleSubmit}>
          <div>
            <label>名前:</label>
            <input 
              type="text" 
              value={name}
              onChange={(e) => setName(e.target.value)}
            />
          </div>
          <div>
            <label>メール:</label>
            <input 
              type="email" 
              value={email}
              onChange={(e) => setEmail(e.target.value)}
            />
          </div>
          <div>
            <label>年齢:</label>
            <input 
              type="number" 
              value={age}
              onChange={(e) => setAge(e.target.value)}
            />
          </div>
          <button type="submit">保存</button>
          <button type="button" onClick={() => setIsEditing(false)}>
            キャンセル
          </button>
        </form>
      ) : (
        <div>
          <p>名前: {name || '未設定'}</p>
          <p>メール: {email || '未設定'}</p>
          <p>年齢: {age || '未設定'}</p>
          <button onClick={() => setIsEditing(true)}>編集</button>
        </div>
      )}
    </div>
  );
}

条件による表示切り替え

{isEditing ? (
  <form>フォーム</form>
) : (
  <div>表示モード</div>
)}

isEditingの値によって、表示する内容を切り替えています。 これを「条件付きレンダリング」と呼びます。

配列やオブジェクトのState更新

配列やオブジェクトのstateを更新するときは、特別な注意が必要です。

配列のstate(TodoリストDISabled)

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [inputText, setInputText] = useState('');
  
  // 新しいタスクを追加
  const addTodo = () => {
    if (inputText.trim()) {
      const newTodo = {
        id: Date.now(),
        text: inputText,
        completed: false
      };
      
      // ❌ 直接変更はダメ
      // todos.push(newTodo);
      
      // ✅ スプレッド演算子で新しい配列を作成
      setTodos([...todos, newTodo]);
      setInputText('');
    }
  };
  
  // タスクの完了状態を切り替え
  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));
  };
  
  return (
    <div>
      <h2>TODO リスト</h2>
      
      <div>
        <input 
          type="text"
          value={inputText}
          onChange={(e) => setInputText(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
          placeholder="新しいタスクを入力"
        />
        <button onClick={addTodo}>追加</button>
      </div>
      
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input 
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span 
              style={{ 
                textDecoration: todo.completed ? 'line-through' : 'none',
                color: todo.completed ? '#999' : '#000'
              }}
            >
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>削除</button>
          </li>
        ))}
      </ul>
      
      <p>
        合計: {todos.length} | 
        完了: {todos.filter(t => t.completed).length} | 
        未完了: {todos.filter(t => !t.completed).length}
      </p>
    </div>
  );
}

なぜスプレッド演算子を使うの?

// ❌ 元の配列を直接変更(Reactが変更を検知できない)
todos.push(newTodo);
setTodos(todos);

// ✅ 新しい配列を作成(Reactが変更を検知できる)
setTodos([...todos, newTodo]);

Reactは、stateが「本当に変更されたか」を判断するために、オブジェクトの参照を比較します。 元の配列を直接変更すると、参照が同じなので変更が検知されません。

イベント処理を理解しよう

Reactでのイベント処理には、いくつかのポイントがあります。

基本的なイベント処理

function EventExample() {
  const [message, setMessage] = useState('');
  
  // クリックイベント
  const handleClick = (e) => {
    console.log('ボタンがクリックされました');
    console.log('イベントオブジェクト:', e);
    setMessage('ボタンがクリックされました!');
  };
  
  // フォーム送信イベント
  const handleSubmit = (e) => {
    e.preventDefault(); // デフォルトの送信を阻止
    console.log('フォームが送信されました');
    setMessage('フォームが送信されました!');
  };
  
  // キー入力イベント
  const handleKeyDown = (e) => {
    if (e.key === 'Enter' && e.ctrlKey) {
      setMessage('Ctrl + Enter が押されました!');
    }
  };
  
  return (
    <div>
      <h2>イベント処理の例</h2>
      <p>メッセージ: {message}</p>
      
      <button onClick={handleClick}>
        クリックしてください
      </button>
      
      <form onSubmit={handleSubmit}>
        <input 
          type="text" 
          placeholder="何か入力してEnterを押してください"
          onKeyDown={handleKeyDown}
        />
        <button type="submit">送信</button>
      </form>
    </div>
  );
}

イベントハンドラーの書き方

// 方法1:関数を定義してから渡す
const handleClick = () => {
  setCount(count + 1);
};
<button onClick={handleClick}>+1</button>

// 方法2:インライン関数
<button onClick={() => setCount(count + 1)}>+1</button>

// 方法3:引数を渡したい場合
<button onClick={() => updateCount(count + 1)}>+1</button>

e.preventDefault()って何?

const handleSubmit = (e) => {
  e.preventDefault(); // ページリロードを阻止
  // ここで独自の処理を実行
};

フォームの送信やリンクのクリックなど、ブラウザのデフォルト動作を止めたいときに使います。

条件付きレンダリングのパターン

stateの値に応じて、表示内容を動的に変更する方法をマスターしましょう。

様々な条件分岐パターン

function ConditionalRenderingExample() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [userType, setUserType] = useState('guest');
  const [notifications, setNotifications] = useState([
    { id: 1, message: '新しいメッセージがあります', type: 'info' },
    { id: 2, message: 'システムメンテナンスのお知らせ', type: 'warning' }
  ]);
  
  return (
    <div>
      <h2>条件付きレンダリング</h2>
      
      {/* パターン1:三項演算子 */}
      {isLoggedIn ? (
        <p>ようこそ!ログイン中です。</p>
      ) : (
        <p>ログインしてください。</p>
      )}
      
      {/* パターン2:論理AND演算子 */}
      {isLoggedIn && <button>ログアウト</button>}
      {!isLoggedIn && (
        <button onClick={() => setIsLoggedIn(true)}>
          ログイン
        </button>
      )}
      
      {/* パターン3:複数条件(switch文) */}
      {(() => {
        switch (userType) {
          case 'admin':
            return <p>管理者として認証されています</p>;
          case 'member':
            return <p>メンバーとしてログインしています</p>;
          case 'guest':
            return <p>ゲストとして閲覧しています</p>;
          default:
            return <p>認証状態を確認中...</p>;
        }
      })()}
      
      {/* パターン4:リストの条件付き表示 */}
      {notifications.length > 0 ? (
        <div>
          <h3>通知 ({notifications.length}件)</h3>
          <ul>
            {notifications.map(notification => (
              <li 
                key={notification.id}
                style={{
                  color: notification.type === 'warning' ? 'orange' : 'blue'
                }}
              >
                {notification.message}
              </li>
            ))}
          </ul>
        </div>
      ) : (
        <p>新しい通知はありません</p>
      )}
      
      {/* 操作ボタン */}
      <div style={{ marginTop: '20px' }}>
        <button onClick={() => setIsLoggedIn(!isLoggedIn)}>
          ログイン状態切り替え
        </button>
        <select 
          value={userType} 
          onChange={(e) => setUserType(e.target.value)}
        >
          <option value="guest">ゲスト</option>
          <option value="member">メンバー</option>
          <option value="admin">管理者</option>
        </select>
      </div>
    </div>
  );
}

どのパターンを使えばいいの?

  • 簡単なon/off{isLoggedIn && <Component />}
  • 2択の表示切り替え{condition ? <A /> : <B />}
  • 複数の条件 → switch文やif文
  • リストの有無{list.length > 0 ? <List /> : <Empty />}

この段階をマスターすると、基本的なインタラクティブアプリが作れるようになります。 ユーザーの操作に応じて画面が変わる、動的なWebアプリケーションの基礎ができますね。

ステップ4:useEffectで副作用を扱おう

useEffectは、Reactの中でも特に重要なHookです。 API呼び出し、タイマー、イベントリスナーなど、コンポーネントの描画以外の処理を管理できます。

useEffectの基本概念

useEffectは「副作用」を管理するためのHookです。 副作用とは、コンポーネントの描画以外の処理のことです。

基本的なuseEffectの使い方

import React, { useState, useEffect } from 'react';

function BasicEffectExample() {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('');
  
  // パターン1:毎回実行される(依存配列なし)
  useEffect(() => {
    console.log('コンポーネントが描画されました');
    document.title = `カウント: ${count}`;
  });
  
  // パターン2:最初だけ実行される(空の依存配列)
  useEffect(() => {
    console.log('コンポーネントがマウントされました');
    setMessage('初期化完了');
  }, []);
  
  // パターン3:countが変更された時だけ実行
  useEffect(() => {
    console.log('countが変更されました:', count);
    
    if (count >= 10) {
      setMessage('カウントが10以上になりました!');
    } else {
      setMessage('カウント中...');
    }
  }, [count]); // countが変更された時のみ実行
  
  return (
    <div>
      <h2>useEffect の基本</h2>
      <p>カウント: {count}</p>
      <p>メッセージ: {message}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(0)}>リセット</button>
    </div>
  );
}

依存配列が重要

useEffect(() => {
  // 何かの処理
}, []); // この配列が「依存配列」

// 空の配列 [] → 最初だけ実行
// [count] → countが変更された時だけ実行
// 配列なし → 毎回実行(通常は避ける)

依存配列を正しく設定することで、パフォーマンスの問題を避けられます。

APIからデータを取得しよう

useEffectを使って、外部のAPIからデータを取得する方法を学びましょう。

基本的なAPI通信

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    // 非同期でユーザーデータを取得
    const fetchUsers = async () => {
      try {
        setLoading(true);
        setError(null);
        
        // テスト用のAPIを使用
        const response = await fetch('https://jsonplaceholder.typicode.com/users');
        
        if (!response.ok) {
          throw new Error('データの取得に失敗しました');
        }
        
        const userData = await response.json();
        setUsers(userData);
      } catch (err) {
        setError(err.message);
        console.error('API Error:', err);
      } finally {
        setLoading(false);
      }
    };
    
    fetchUsers();
  }, []); // 空の依存配列で初回のみ実行
  
  // ローディング中の表示
  if (loading) {
    return <div>ユーザーデータを読み込み中...</div>;
  }
  
  // エラー時の表示
  if (error) {
    return <div>エラー: {error}</div>;
  }
  
  // データ表示
  return (
    <div>
      <h2>ユーザー一覧</h2>
      <div>
        {users.map(user => (
          <div key={user.id} style={{ 
            border: '1px solid #ddd', 
            margin: '10px', 
            padding: '10px' 
          }}>
            <h3>{user.name}</h3>
            <p>ユーザー名: {user.username}</p>
            <p>メール: {user.email}</p>
            <p>電話: {user.phone}</p>
            <p>ウェブサイト: {user.website}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

この処理の流れ

  1. コンポーネントがマウントされる
  2. useEffectが実行される
  3. APIからデータを取得
  4. 取得したデータでstateを更新
  5. 画面が再描画される

エラーハンドリングも重要

try {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error('データの取得に失敗しました');
  }
  const data = await response.json();
  setData(data);
} catch (error) {
  setError(error.message);
} finally {
  setLoading(false);
}
  • try-catchでエラーをキャッチ
  • finallyで必ずローディング状態を解除
  • ユーザーにエラーメッセージを表示

カスタムHookで再利用可能にしよう

// カスタムHook: useFetch
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const fetchData = async () => {
      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);
      }
    };
    
    if (url) {
      fetchData();
    }
  }, [url]);
  
  return { data, loading, error };
}

// カスタムHookの使用例
function PostList() {
  const { data: posts, loading, error } = useFetch(
    'https://jsonplaceholder.typicode.com/posts'
  );
  
  if (loading) return <div>投稿を読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  
  return (
    <div>
      <h2>投稿一覧</h2>
      {posts && posts.slice(0, 5).map(post => (
        <article key={post.id} style={{
          border: '1px solid #eee',
          margin: '10px 0',
          padding: '15px'
        }}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </article>
      ))}
    </div>
  );
}

カスタムHookのメリット

  • 同じロジックを複数のコンポーネントで使い回せる
  • コンポーネントがスッキリする
  • テストしやすくなる

クリーンアップ処理を理解しよう

useEffectでは、メモリリークを防ぐためのクリーンアップ処理を定義できます。

タイマーのクリーンアップ

function Timer() {
  const [seconds, setSeconds] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  
  useEffect(() => {
    let intervalId;
    
    if (isRunning) {
      intervalId = setInterval(() => {
        setSeconds(prev => prev + 1);
      }, 1000);
    }
    
    // クリーンアップ関数
    return () => {
      if (intervalId) {
        clearInterval(intervalId);
      }
    };
  }, [isRunning]); // isRunningが変更された時に実行
  
  const handleStart = () => setIsRunning(true);
  const handleStop = () => setIsRunning(false);
  const handleReset = () => {
    setIsRunning(false);
    setSeconds(0);
  };
  
  const formatTime = (secs) => {
    const minutes = Math.floor(secs / 60);
    const remainingSeconds = secs % 60;
    return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
  };
  
  return (
    <div>
      <h2>タイマー</h2>
      <div style={{ fontSize: '2em', marginBottom: '20px' }}>
        {formatTime(seconds)}
      </div>
      <button onClick={handleStart} disabled={isRunning}>
        開始
      </button>
      <button onClick={handleStop} disabled={!isRunning}>
        停止
      </button>
      <button onClick={handleReset}>
        リセット
      </button>
    </div>
  );
}

クリーンアップが必要な理由

// クリーンアップしないと...
useEffect(() => {
  const intervalId = setInterval(() => {
    console.log('まだ動いてる...');
  }, 1000);
  
  // return文がないと、タイマーが止まらない!
}, []);

コンポーネントがアンマウントされても、タイマーが動き続けてしまいます。 これがメモリリークの原因になります。

イベントリスナーのクリーンアップ

function WindowSizeTracker() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });
  
  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    
    // イベントリスナーを追加
    window.addEventListener('resize', handleResize);
    
    // クリーンアップ関数でイベントリスナーを削除
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空の依存配列で初回のみ実行
  
  return (
    <div>
      <h2>ウィンドウサイズ</h2>
      <p>幅: {windowSize.width}px</p>
      <p>高さ: {windowSize.height}px</p>
      <p>ウィンドウサイズを変更してみてください</p>
    </div>
  );
}

実践的なuseEffect活用例

複数のuseEffectを組み合わせた実践的な例を見てみましょう。

デバウンス機能付きリアルタイム検索

function SearchUsers() {
  const [query, setQuery] = useState('');
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  // デバウンス機能付きの検索
  useEffect(() => {
    if (!query.trim()) {
      setUsers([]);
      return;
    }
    
    const timeoutId = setTimeout(() => {
      searchUsers(query);
    }, 500); // 500ms待ってから検索実行
    
    return () => clearTimeout(timeoutId);
  }, [query]);
  
  const searchUsers = async (searchQuery) => {
    try {
      setLoading(true);
      setError(null);
      
      // 実際のAPIでは検索クエリを使用
      const response = await fetch('https://jsonplaceholder.typicode.com/users');
      const allUsers = await response.json();
      
      // クライアントサイドでフィルタリング
      const filteredUsers = allUsers.filter(user =>
        user.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
        user.email.toLowerCase().includes(searchQuery.toLowerCase())
      );
      
      setUsers(filteredUsers);
    } catch (err) {
      setError('検索中にエラーが発生しました');
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <div>
      <h2>ユーザー検索</h2>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="ユーザー名またはメールアドレスで検索"
        style={{
          width: '300px',
          padding: '8px',
          fontSize: '16px',
          marginBottom: '20px'
        }}
      />
      
      {loading && <p>検索中...</p>}
      {error && <p style={{ color: 'red' }}>{error}</p>}
      
      <div>
        {users.map(user => (
          <div key={user.id} style={{
            border: '1px solid #ddd',
            padding: '10px',
            margin: '5px 0'
          }}>
            <strong>{user.name}</strong> - {user.email}
          </div>
        ))}
        {query && !loading && users.length === 0 && (
          <p>検索結果が見つかりませんでした</p>
        )}
      </div>
    </div>
  );
}

デバウンスって何?

ユーザーが入力を止めてから一定時間後に検索を実行する仕組みです。 入力のたびに検索すると、APIに負荷がかかってしまうからです。

// 入力のたび → setTimeout設定
// 500ms以内に再入力 → 前のタイマーをクリア
// 500ms経過 → 検索実行

これで効率的な検索機能が実現できます。

useEffectをマスターすると、API通信やタイマーなど、実用的なWebアプリケーションで必要な機能を実装できるようになります。 ここまで来れば、基本的なReactアプリケーション開発のスキルが身についたと言えますね。

ステップ5:実践プロジェクトで力をつけよう

今まで学んだ知識を組み合わせて、実際に動くWebアプリケーションを作ってみましょう。 「勉強」から「開発」に変わる、ワクワクするステップです。

何を作るか決めよう

実用的で楽しく作れるプロジェクトを選ぶのがコツです。

おすすめプロジェクト:高機能ToDo管理アプリ

基本的なToDo機能だけでなく、こんな機能も追加してみます。

  • タスクの追加・編集・削除
  • カテゴリ分類(仕事、プライベートなど)
  • 優先度設定(高・中・低)
  • 検索・フィルタリング
  • データ保存(ブラウザのLocal Storage)
  • レスポンシブデザイン(スマホでも使える)

なぜこのプロジェクトがいいの?

  • 実用性がある → 実際に使えるアプリ
  • 段階的に機能追加できる → 挫折しにくい
  • 様々な技術を練習できる → 総合的なスキルアップ

プロジェクト構成を考えよう

src/
├── components/           # コンポーネント
│   ├── TaskForm.jsx     # タスク入力フォーム
│   ├── TaskList.jsx     # タスク一覧表示
│   ├── TaskItem.jsx     # 個別タスク
│   ├── FilterBar.jsx    # 検索・フィルター
│   └── Header.jsx       # ヘッダー
├── hooks/               # カスタムHook
│   ├── useLocalStorage.jsx # データ保存管理
│   └── useTasks.jsx     # タスク管理ロジック
├── utils/               # ユーティリティ関数
│   └── helpers.js       # 便利な関数
└── App.jsx              # メインアプリ

コンポーネントを小さく分けることで、管理しやすくなります。

カスタムHookを作ってみよう

再利用できるロジックをカスタムHookとして分離してみましょう。

useLocalStorage Hook(データ保存用)

// hooks/useLocalStorage.jsx
import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // Local Storageから初期値を取得
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error('Local Storage read error:', error);
      return initialValue;
    }
  });
  
  // 値を設定し、Local Storageにも保存
  const setValue = (value) => {
    try {
      // 関数が渡された場合は実行
      const valueToStore = value instanceof Function 
        ? value(storedValue) 
        : value;
      
      setStoredValue(valueToStore);
      
      // Local Storageに保存
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error('Local Storage write error:', error);
    }
  };
  
  return [storedValue, setValue];
}

export default useLocalStorage;

このHookの使い方

function MyComponent() {
  // 普通のuseStateと同じように使える
  const [tasks, setTasks] = useLocalStorage('tasks', []);
  
  // でも、データが自動でLocal Storageに保存される!
  const addTask = (newTask) => {
    setTasks([...tasks, newTask]);
  };
  
  return <div>...</div>;
}

useTasks Hook(タスク管理ロジック)

// hooks/useTasks.jsx
import { useState, useMemo } from 'react';
import useLocalStorage from './useLocalStorage';

function useTasks() {
  const [tasks, setTasks] = useLocalStorage('tasks', []);
  const [filter, setFilter] = useState('all');
  const [searchQuery, setSearchQuery] = useState('');
  const [sortBy, setSortBy] = useState('created');
  
  // タスク追加
  const addTask = (taskData) => {
    const newTask = {
      id: Date.now(),
      title: taskData.title,
      description: taskData.description || '',
      category: taskData.category || 'general',
      priority: taskData.priority || 'medium',
      completed: false,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    };
    
    setTasks([...tasks, newTask]);
  };
  
  // タスク更新
  const updateTask = (id, updates) => {
    setTasks(tasks.map(task => 
      task.id === id 
        ? { ...task, ...updates, updatedAt: new Date().toISOString() }
        : task
    ));
  };
  
  // タスク削除
  const deleteTask = (id) => {
    setTasks(tasks.filter(task => task.id !== id));
  };
  
  // タスク完了切り替え
  const toggleTask = (id) => {
    const task = tasks.find(t => t.id === id);
    if (task) {
      updateTask(id, { completed: !task.completed });
    }
  };
  
  // フィルタリングとソート済みタスク
  const filteredAndSortedTasks = useMemo(() => {
    let result = tasks;
    
    // 検索フィルター
    if (searchQuery) {
      result = result.filter(task =>
        task.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
        task.description.toLowerCase().includes(searchQuery.toLowerCase())
      );
    }
    
    // ステータスフィルター
    switch (filter) {
      case 'active':
        result = result.filter(task => !task.completed);
        break;
      case 'completed':
        result = result.filter(task => task.completed);
        break;
      default:
        break;
    }
    
    // ソート
    result.sort((a, b) => {
      switch (sortBy) {
        case 'priority':
          const priorityOrder = { high: 3, medium: 2, low: 1 };
          return priorityOrder[b.priority] - priorityOrder[a.priority];
        case 'title':
          return a.title.localeCompare(b.title);
        case 'created':
        default:
          return new Date(b.createdAt) - new Date(a.createdAt);
      }
    });
    
    return result;
  }, [tasks, filter, searchQuery, sortBy]);
  
  // 統計情報
  const stats = useMemo(() => ({
    total: tasks.length,
    completed: tasks.filter(t => t.completed).length,
    active: tasks.filter(t => !t.completed).length,
    highPriority: tasks.filter(t => t.priority === 'high' && !t.completed).length
  }), [tasks]);
  
  return {
    tasks: filteredAndSortedTasks,
    stats,
    filter,
    setFilter,
    searchQuery,
    setSearchQuery,
    sortBy,
    setSortBy,
    addTask,
    updateTask,
    deleteTask,
    toggleTask
  };
}

export default useTasks;

useMemoって何?

// 重い計算処理をメモ化(記憶)
const expensiveValue = useMemo(() => {
  // 時間のかかる処理
  return heavyCalculation(data);
}, [data]); // dataが変わった時だけ再計算

依存配列の値が変わらない限り、前回の計算結果を再利用します。 パフォーマンス向上に役立ちます。

コンポーネントを実装しよう

各コンポーネントを実装していきます。

TaskForm.jsx(入力フォーム)

// components/TaskForm.jsx
import React, { useState } from 'react';

const CATEGORIES = [
  { value: 'work', label: '仕事' },
  { value: 'personal', label: 'プライベート' },
  { value: 'shopping', label: '買い物' },
  { value: 'health', label: '健康' },
  { value: 'general', label: 'その他' }
];

const PRIORITIES = [
  { value: 'low', label: '低' },
  { value: 'medium', label: '中' },
  { value: 'high', label: '高' }
];

function TaskForm({ onSubmit, initialData = null }) {
  const [formData, setFormData] = useState({
    title: initialData?.title || '',
    description: initialData?.description || '',
    category: initialData?.category || 'general',
    priority: initialData?.priority || 'medium'
  });
  
  const [errors, setErrors] = useState({});
  
  // バリデーション
  const validateForm = () => {
    const newErrors = {};
    
    if (!formData.title.trim()) {
      newErrors.title = 'タイトルは必須です';
    }
    
    if (formData.title.length > 100) {
      newErrors.title = 'タイトルは100文字以内で入力してください';
    }
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (validateForm()) {
      onSubmit(formData);
      
      // 新規作成の場合はフォームをリセット
      if (!initialData) {
        setFormData({
          title: '',
          description: '',
          category: 'general',
          priority: 'medium'
        });
      }
    }
  };
  
  const handleChange = (field) => (e) => {
    setFormData({
      ...formData,
      [field]: e.target.value
    });
    
    // エラーをクリア
    if (errors[field]) {
      setErrors({
        ...errors,
        [field]: ''
      });
    }
  };
  
  return (
    <form onSubmit={handleSubmit} className="task-form">
      <div className="form-group">
        <label htmlFor="title">タイトル *</label>
        <input
          type="text"
          id="title"
          value={formData.title}
          onChange={handleChange('title')}
          className={errors.title ? 'error' : ''}
          placeholder="タスクのタイトルを入力"
        />
        {errors.title && <span className="error-message">{errors.title}</span>}
      </div>
      
      <div className="form-group">
        <label htmlFor="description">説明</label>
        <textarea
          id="description"
          value={formData.description}
          onChange={handleChange('description')}
          placeholder="タスクの詳細説明(任意)"
          rows="3"
        />
      </div>
      
      <div className="form-row">
        <div className="form-group">
          <label htmlFor="category">カテゴリ</label>
          <select
            id="category"
            value={formData.category}
            onChange={handleChange('category')}
          >
            {CATEGORIES.map(cat => (
              <option key={cat.value} value={cat.value}>
                {cat.label}
              </option>
            ))}
          </select>
        </div>
        
        <div className="form-group">
          <label htmlFor="priority">優先度</label>
          <select
            id="priority"
            value={formData.priority}
            onChange={handleChange('priority')}
          >
            {PRIORITIES.map(pri => (
              <option key={pri.value} value={pri.value}>
                {pri.label}
              </option>
            ))}
          </select>
        </div>
      </div>
      
      <button type="submit" className="submit-btn">
        {initialData ? '更新' : '追加'}
      </button>
    </form>
  );
}

export default TaskForm;

TaskItem.jsx(個別タスク表示)

// components/TaskItem.jsx
import React, { useState } from 'react';
import TaskForm from './TaskForm';

function TaskItem({ task, onToggle, onDelete, onUpdate }) {
  const [isEditing, setIsEditing] = useState(false);
  
  const handleUpdate = (updatedData) => {
    onUpdate(task.id, updatedData);
    setIsEditing(false);
  };
  
  const getPriorityColor = (priority) => {
    switch (priority) {
      case 'high': return '#ff4757';
      case 'medium': return '#ffa502';
      case 'low': return '#2ed573';
      default: return '#747d8c';
    }
  };
  
  const formatDate = (dateString) => {
    return new Date(dateString).toLocaleDateString('ja-JP', {
      year: 'numeric',
      month: 'short',
      day: 'numeric',
      hour: '2-digit',
      minute: '2-digit'
    });
  };
  
  // 編集モードの場合
  if (isEditing) {
    return (
      <div className="task-item editing">
        <TaskForm
          onSubmit={handleUpdate}
          initialData={task}
        />
        <button
          onClick={() => setIsEditing(false)}
          className="cancel-btn"
        >
          キャンセル
        </button>
      </div>
    );
  }
  
  // 通常表示の場合
  return (
    <div className={`task-item ${task.completed ? 'completed' : ''}`}>
      <div className="task-content">
        <div className="task-header">
          <input
            type="checkbox"
            checked={task.completed}
            onChange={() => onToggle(task.id)}
            className="task-checkbox"
          />
          <h3 className="task-title">{task.title}</h3>
          <span
            className="priority-badge"
            style={{ backgroundColor: getPriorityColor(task.priority) }}
          >
            {task.priority}
          </span>
        </div>
        
        {task.description && (
          <p className="task-description">{task.description}</p>
        )}
        
        <div className="task-meta">
          <span className="category">{task.category}</span>
          <span className="created-date">
            作成: {formatDate(task.createdAt)}
          </span>
          {task.updatedAt !== task.createdAt && (
            <span className="updated-date">
              更新: {formatDate(task.updatedAt)}
            </span>
          )}
        </div>
      </div>
      
      <div className="task-actions">
        <button
          onClick={() => setIsEditing(true)}
          className="edit-btn"
          disabled={task.completed}
        >
          編集
        </button>
        <button
          onClick={() => onDelete(task.id)}
          className="delete-btn"
        >
          削除
        </button>
      </div>
    </div>
  );
}

export default TaskItem;

メインアプリで全てを組み合わせよう

全てのコンポーネントとHookを組み合わせてメインアプリを作ります。

App.jsx

// App.jsx
import React from 'react';
import TaskForm from './components/TaskForm';
import TaskList from './components/TaskList';
import FilterBar from './components/FilterBar';
import Header from './components/Header';
import useTasks from './hooks/useTasks';
import './App.css';

function App() {
  const {
    tasks,
    stats,
    filter,
    setFilter,
    searchQuery,
    setSearchQuery,
    sortBy,
    setSortBy,
    addTask,
    updateTask,
    deleteTask,
    toggleTask
  } = useTasks();
  
  return (
    <div className="app">
      <Header stats={stats} />
      
      <main className="main-content">
        <section className="task-form-section">
          <h2>新しいタスクを追加</h2>
          <TaskForm onSubmit={addTask} />
        </section>
        
        <section className="task-list-section">
          <FilterBar
            filter={filter}
            onFilterChange={setFilter}
            searchQuery={searchQuery}
            onSearchChange={setSearchQuery}
            sortBy={sortBy}
            onSortChange={setSortBy}
            taskCount={tasks.length}
          />
          
          <TaskList
            tasks={tasks}
            onToggle={toggleTask}
            onDelete={deleteTask}
            onUpdate={updateTask}
          />
        </section>
      </main>
    </div>
  );
}

export default App;

これで完成!

この実践プロジェクトを通じて、今まで学んだReactの知識を統合的に活用できました。

  • useState → 様々な状態管理
  • useEffect → データの保存と復元
  • カスタムHook → ロジックの再利用
  • コンポーネント設計 → 適切な役割分担
  • Props → データの受け渡し

実際に動くアプリケーションを作ることで、「理解」から「実践」に変わったはずです。

次のステップ:さらなる成長へ

基本的なReactスキルを習得できましたね! ここからは、より高度で実用的な内容に進んでいきましょう。

React Router で複数ページアプリを作ろう

今度は複数のページを持つアプリケーションを作ってみます。

React Router の基本

// App.jsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import Navigation from './components/Navigation';
import Home from './pages/Home';
import Tasks from './pages/Tasks';
import Profile from './pages/Profile';
import NotFound from './pages/NotFound';

function App() {
  return (
    <Router>
      <div className="app">
        <Navigation />
        <main className="main-content">
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/tasks" element={<Tasks />} />
            <Route path="/profile" element={<Profile />} />
            <Route path="/404" element={<NotFound />} />
            <Route path="*" element={<Navigate to="/404" replace />} />
          </Routes>
        </main>
      </div>
    </Router>
  );
}

export default App;

動的ルーティング

// URLのパラメータを受け取る
<Routes>
  <Route path="/tasks/:id" element={<TaskDetail />} />
  <Route path="/categories/:category" element={<CategoryTasks />} />
</Routes>

// TaskDetail.jsx
import React from 'react';
import { useParams, useNavigate } from 'react-router-dom';

function TaskDetail() {
  const { id } = useParams(); // URLから id を取得
  const navigate = useNavigate(); // プログラムでページ遷移
  
  const handleBack = () => {
    navigate('/tasks');
  };
  
  return (
    <div>
      <h1>タスク詳細 (ID: {id})</h1>
      <button onClick={handleBack}>戻る</button>
    </div>
  );
}

これで、本格的なWebサイトのような画面遷移ができるようになります。

Context API でグローバル状態管理

アプリ全体で共有したいデータがある場合、Context API が便利です。

テーマ管理のContext

// contexts/ThemeContext.jsx
import React, { createContext, useContext, useState } from 'react';

const ThemeContext = createContext();

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
  
  const value = {
    theme,
    toggleTheme,
    colors: theme === 'light' ? lightColors : darkColors
  };
  
  return (
    <ThemeContext.Provider value={value}>
      <div className={`app-theme-${theme}`}>
        {children}
      </div>
    </ThemeContext.Provider>
  );
}

// 使用例
function Header() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <header>
      <h1>My App</h1>
      <button onClick={toggleTheme}>
        {theme === 'light' ? '🌙' : '☀️'}
      </button>
    </header>
  );
}

パフォーマンス最適化も学ぼう

アプリが大きくなってきたら、パフォーマンス最適化も重要です。

React.memo で無駄な再レンダリングを防ぐ

// 最適化前:親が再レンダリングされると毎回再レンダリング
function TaskItem({ task, onToggle, onDelete }) {
  console.log('TaskItem rendered:', task.id);
  return (
    <div>
      <h3>{task.title}</h3>
      <button onClick={() => onToggle(task.id)}>Toggle</button>
      <button onClick={() => onDelete(task.id)}>Delete</button>
    </div>
  );
}

// 最適化後:propsが変わった時だけ再レンダリング
const TaskItem = React.memo(function TaskItem({ task, onToggle, onDelete }) {
  console.log('TaskItem rendered:', task.id);
  return (
    <div>
      <h3>{task.title}</h3>
      <button onClick={() => onToggle(task.id)}>Toggle</button>
      <button onClick={() => onDelete(task.id)}>Delete</button>
    </div>
  );
});

useCallback で関数をメモ化

function TaskList({ tasks }) {
  // 関数をメモ化して子コンポーネントの無駄な再レンダリングを防ぐ
  const handleToggle = useCallback((id) => {
    // タスクのトグル処理
  }, []);
  
  const handleDelete = useCallback((id) => {
    // タスクの削除処理
  }, []);
  
  return (
    <div>
      {tasks.map(task => (
        <TaskItem
          key={task.id}
          task={task}
          onToggle={handleToggle}
          onDelete={handleDelete}
        />
      ))}
    </div>
  );
}

テストも書いてみよう

品質の高いアプリを作るために、テストの基礎も学びましょう。

コンポーネントテスト

// TaskItem.test.jsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import TaskItem from './TaskItem';

const mockTask = {
  id: 1,
  title: 'テストタスク',
  completed: false,
  priority: 'medium'
};

describe('TaskItem', () => {
  const mockOnToggle = jest.fn();
  const mockOnDelete = jest.fn();
  
  beforeEach(() => {
    mockOnToggle.mockClear();
    mockOnDelete.mockClear();
  });
  
  test('タスクのタイトルが表示される', () => {
    render(
      <TaskItem
        task={mockTask}
        onToggle={mockOnToggle}
        onDelete={mockOnDelete}
      />
    );
    
    expect(screen.getByText('テストタスク')).toBeInTheDocument();
  });
  
  test('チェックボックスをクリックするとonToggleが呼ばれる', () => {
    render(
      <TaskItem
        task={mockTask}
        onToggle={mockOnToggle}
        onDelete={mockOnDelete}
      />
    );
    
    const checkbox = screen.getByRole('checkbox');
    fireEvent.click(checkbox);
    
    expect(mockOnToggle).toHaveBeenCalledWith(1);
  });
});

学習のポイント

  • 基礎をしっかり固めてから発展的な内容に進む
  • 実際にプロジェクトを作って経験を積む
  • 必要に応じて新しい技術を学習する
  • コミュニティで他の開発者と交流する

おすすめの学習リソース

公式ドキュメント

  • React 公式ドキュメント → 最新で正確な情報
  • React ブログ → 新機能の情報
  • GitHub → ソースコードと実例

学習コンテンツ

  • freeCodeCamp → 無料の実践的な学習
  • Udemy → 体系的な有料コース
  • YouTube → 動画による学習
  • Qiita → 日本語の技術記事

コミュニティ

  • React 勉強会 → 実際の勉強会に参加
  • Discord/Slack → オンラインコミュニティ
  • Twitter → 最新情報のキャッチアップ
  • Stack Overflow → 技術的な質問と回答

継続的な学習と実践により、プロレベルのReact開発者になれるはずです。

まとめ:挫折しないReact学習の秘訣

React学習の正しい順序について、詳しく解説してきました。

学習順序の重要ポイント

  1. JavaScript基礎を固める → Reactの土台作り
  2. React基礎概念を理解 → JSX、コンポーネント、Props
  3. State管理とイベント処理 → 動的なアプリの基本
  4. useEffectで副作用を扱う → API通信や外部連携
  5. 実践プロジェクトで総合力 → 学んだ知識を統合

挫折しないための心構え

  • 焦らない → 一つずつ確実に理解する
  • 手を動かす → 実際にコードを書いて覚える
  • エラーを恐れない → 失敗は学習のチャンス
  • 完璧を求めない → 動くものを作ることを優先
  • 継続する → 少しずつでも毎日続ける

今すぐできること

  • Create React App でプロジェクトを作ってみる
  • 簡単なコンポーネントから始める
  • Todoアプリなど実用的なものを作る
  • React Developer Tools をインストールする
  • コミュニティに参加して情報交換する

最後に大切なメッセージ

React学習は「習うより慣れろ」です。 理論ばかり勉強するより、実際にコードを書いて試行錯誤する方が身につきます。

最初は分からないことだらけでも大丈夫。 正しい順序で学習を進めれば、必ずマスターできます。

あなたのReact学習が成功することを、心から応援しています! 素晴らしいWebアプリケーションを作る日が楽しみですね。

関連記事