Reactチュートリアル|公式より分かりやすい入門ガイド

React初心者向けの分かりやすいチュートリアル。公式ドキュメントよりも理解しやすく、実際のコード例とともにReactの基本から実用的なアプリケーション作成まで段階的に学べる完全ガイドです。

Learning Next 運営
84 分で読めます

React学習で挫折したことはありませんか?

公式ドキュメントを読んでも「難しくてよく分からない」 「もっと実践的な例で学びたい」 そんな悩みを抱えている方も多いですよね。

実は、Reactの基本概念さえ理解できれば、実用的なWebアプリが作れるようになるんです。

この記事では、React公式ドキュメントよりも分かりやすく、コード例をたくさん使って学べるチュートリアルを用意しました。 完全初心者から始めて、最終的には本格的なTodoアプリが作れるまで、一歩ずつ進めていきますよ。

Reactって何?なぜ学ぶべきなの?

まず、Reactとは何かを理解しましょう。

Reactの正体を知ろう

ReactはFacebookが開発したJavaScriptライブラリです。 Webページのユーザーインターフェース(UI)を作るためのツールだと考えてください。

従来のWebページ作成方法

<!-- 昔ながらのHTML -->
<div id="counter">
  <h2>カウンター</h2>
  <p>カウント: <span id="count">0</span></p>
  <button onclick="increment()">+1</button>
</div>

<script>
let count = 0;

function increment() {
  count++;
  document.getElementById('count').textContent = count;
}
</script>

従来の方法では、データが変わるたびに手動でHTMLを更新していました。 これって結構面倒ですよね。

Reactでの書き方

// React版(すっきり!)
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>カウンター</h2>
      <p>カウント: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

Reactでは、データが変わると自動的にHTMLが更新されます。 便利ですよね!

Reactを学ぶとこんなメリットが

1. 開発が楽になる

  • コンポーネントを使い回せる
  • データの変更が自動的にUIに反映される
  • バグが見つけやすい

2. 就職・転職に有利

  • 多くの企業で採用されている
  • 求人数が豊富
  • 高収入を期待できる

3. 豊富な学習リソース

  • 大量のライブラリが利用可能
  • 教材が豊富
  • コミュニティが活発

学習の前提知識

React学習には、以下の知識が必要です。

絶対に必要な知識

  • HTML(基本的なタグ)
  • CSS(基本的なスタイリング)
  • JavaScript(変数、関数、配列、オブジェクト)

あるとさらに良い知識

  • ES6の構文(アロー関数、分割代入など)
  • DOM操作の基本
  • 非同期処理の概念
// 最低限知っておきたいJavaScript
// 1. 変数
const name = '太郎';
let age = 25;

// 2. 関数
function greet(name) {
  return `こんにちは、${name}さん`;
}

// 3. アロー関数
const greet2 = (name) => `こんにちは、${name}さん`;

// 4. 配列
const fruits = ['りんご', 'バナナ', '梨'];

// 5. オブジェクト
const user = {
  name: '太郎',
  age: 25,
  email: 'taro@example.com'
};

// 6. 分割代入
const { name, age } = user;
const [first, second] = fruits;

これらの基本的な知識があれば大丈夫です。 心配いりません!

環境構築|一番簡単な始め方

Reactを始めるための環境構築を、最も簡単な方法から紹介します。

方法1: オンラインエディタ(今すぐ始められる)

まずは何もインストールせずに、ブラウザだけでReactを試してみましょう。

CodeSandbox を使おう

  1. https://codesandbox.io/ にアクセス
  2. 「Create Sandbox」をクリック
  3. 「React」テンプレートを選択
  4. 即座にReact環境が使える
// CodeSandbox で試せる最初のコード
import React from 'react';
import ReactDOM from 'react-dom';

function App() {
  return (
    <div>
      <h1>初めてのReactアプリ</h1>
      <p>環境構築不要で即座に始められます!</p>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));

これで今すぐReactが体験できます。 簡単ですよね。

方法2: ローカル環境構築(本格的な開発)

本格的にReactを学習する場合は、ローカル環境を構築しましょう。

Node.js をインストール

# Node.js のバージョン確認
node --version
npm --version

# インストールされていない場合
# https://nodejs.org/ から最新LTS版をダウンロード

Node.jsがインストールできたら次に進みます。

Create React App でプロジェクト作成

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

# プロジェクトディレクトリに移動
cd my-first-react-app

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

コマンドを実行すると、ブラウザが自動で開きます。 Reactアプリが動いているのが確認できるはずです。

プロジェクト構造を理解しよう

my-first-react-app/
├── public/
│   ├── index.html    # メインHTMLファイル
│   └── favicon.ico   # ファビコン
├── src/
│   ├── App.js        # メインコンポーネント
│   ├── App.css       # スタイルファイル
│   ├── index.js      # エントリーポイント
│   └── index.css     # グローバルスタイル
├── package.json      # プロジェクト設定
└── README.md         # プロジェクト説明

最初のコードを理解する

Create React App で作成されたファイルを見てみましょう。

src/index.js(アプリケーションの起点)

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

// React 18 の新しい書き方
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

このファイルがReactアプリの出発点です。 Appコンポーネントを画面に表示しています。

src/App.js(メインコンポーネント)

import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <h1>Welcome to React</h1>
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
      </header>
    </div>
  );
}

export default App;

これがメインのコンポーネントです。 ここを編集することでアプリの内容を変更できます。

public/index.html(HTMLテンプレート)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>React App</title>
</head>
<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root"></div>
  <!-- ここにReactアプリケーションが描画される -->
</body>
</html>

このHTMLファイルの<div id="root"></div>の中に、Reactアプリが表示されます。

初回カスタマイズをしてみよう

環境が整ったら、簡単なカスタマイズをしてみましょう。

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

function App() {
  // 現在の日時を取得
  const currentTime = new Date().toLocaleString();

  return (
    <div className="App">
      <header className="App-header">
        <h1>私の初めてのReactアプリ</h1>
        <p>現在の時刻: {currentTime}</p>
        <p>Reactの学習を始めました!</p>
      </header>
    </div>
  );
}

export default App;

ファイルを保存すると、ブラウザが自動的に更新されます。 変更が即座に反映されるのがReactの「ホットリロード」機能です。

これで基本的な環境構築は完了です。

JSXの基本|HTMLに似ているけど違う

ReactではJSXという特殊な構文を使ってUIを記述します。

JSXって何?

JSXは「JavaScript XML」の略です。 JavaScriptの中にHTML風の記述ができる構文なんです。

// JSX の例
const element = <h1>Hello, World!</h1>;

// 上記は実際には以下のJavaScriptコードに変換される
const element = React.createElement('h1', null, 'Hello, World!');

JSXを使うことで、UIを直感的に記述できます。

JSXの基本ルール

1. 必ず一つの要素で囲む

// ❌ 間違い:複数の要素を並べられない
function App() {
  return (
    <h1>タイトル</h1>
    <p>説明文</p>
  );
}

// ✅ 正しい:一つの要素で囲む
function App() {
  return (
    <div>
      <h1>タイトル</h1>
      <p>説明文</p>
    </div>
  );
}

// ✅ React Fragment を使う方法
function App() {
  return (
    <>
      <h1>タイトル</h1>
      <p>説明文</p>
    </>
  );
}

React Fragmentを使うと、余計な<div>を作らずに済みます。

2. クラス名は className を使う

// ❌ 間違い:HTMLのclass属性
function App() {
  return <div class="container">コンテンツ</div>;
}

// ✅ 正しい:JSXのclassName
function App() {
  return <div className="container">コンテンツ</div>;
}

HTMLではclassですが、JSXではclassNameを使います。

3. JavaScriptを埋め込むには {} を使う

function App() {
  const userName = '太郎';
  const age = 25;
  const isAdult = age >= 18;

  return (
    <div>
      <h1>こんにちは、{userName}さん</h1>
      <p>年齢: {age}歳</p>
      <p>大人: {isAdult ? 'はい' : 'いいえ'}</p>
    </div>
  );
}

{}の中にJavaScriptのコードを書けます。

JSXでよく使う表現パターン

条件分岐の表示

function UserProfile({ user }) {
  return (
    <div>
      <h2>ユーザープロフィール</h2>
      
      {/* 条件演算子を使った表示切り替え */}
      {user ? (
        <div>
          <p>名前: {user.name}</p>
          <p>メール: {user.email}</p>
        </div>
      ) : (
        <p>ユーザー情報が見つかりません</p>
      )}
      
      {/* 論理AND演算子を使った条件表示 */}
      {user && user.isAdmin && (
        <p style={{ color: 'red' }}>管理者ユーザーです</p>
      )}
    </div>
  );
}

条件によって表示を切り替えられます。

リスト表示

function TodoList() {
  const todos = [
    { id: 1, text: '買い物をする', completed: false },
    { id: 2, text: '勉強する', completed: true },
    { id: 3, text: '運動する', completed: false }
  ];

  return (
    <div>
      <h2>Todo リスト</h2>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            {/* 完了状態に応じてスタイルを変更 */}
            <span 
              style={{ 
                textDecoration: todo.completed ? 'line-through' : 'none',
                color: todo.completed ? '#888' : '#000'
              }}
            >
              {todo.text}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

配列のデータをリスト表示できます。 key属性は必須です。

イベントハンドリング

function InteractiveComponent() {
  const handleClick = () => {
    alert('ボタンがクリックされました!');
  };

  const handleInputChange = (event) => {
    console.log('入力値:', event.target.value);
  };

  return (
    <div>
      <h2>インタラクティブコンポーネント</h2>
      
      {/* クリックイベント */}
      <button onClick={handleClick}>
        クリックしてください
      </button>
      
      {/* 入力イベント */}
      <input 
        type="text" 
        onChange={handleInputChange}
        placeholder="何か入力してください"
      />
      
      {/* インライン関数 */}
      <button onClick={() => console.log('インラインクリック')}>
        ログ出力
      </button>
    </div>
  );
}

ユーザーの操作に反応するイベントを処理できます。

スタイルの適用方法

インラインスタイル

function StyledComponent() {
  const buttonStyle = {
    backgroundColor: '#007bff',
    color: 'white',
    border: 'none',
    padding: '10px 20px',
    borderRadius: '5px',
    cursor: 'pointer'
  };

  return (
    <div>
      <h2 style={{ color: '#333', fontSize: '24px' }}>
        スタイル付きタイトル
      </h2>
      
      <button style={buttonStyle}>
        スタイル付きボタン
      </button>
    </div>
  );
}

JavaScriptオブジェクトでスタイルを指定できます。

CSSクラスを使用

/* App.css */
.custom-button {
  background-color: #28a745;
  color: white;
  border: none;
  padding: 12px 24px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 16px;
  transition: background-color 0.3s ease;
}

.custom-button:hover {
  background-color: #218838;
}

.highlight {
  background-color: yellow;
  padding: 4px;
}
import './App.css';

function StyledWithCSS() {
  return (
    <div>
      <h2>CSS クラスを使ったスタイリング</h2>
      
      <button className="custom-button">
        CSSクラス付きボタン
      </button>
      
      <p>
        この文章の<span className="highlight">この部分</span>はハイライトされています。
      </p>
    </div>
  );
}

CSSファイルでスタイルを定義して適用することもできます。

JSXを理解することで、Reactの柔軟で強力なUI構築能力を活用できるようになります。

コンポーネント|再利用可能なUIの部品

Reactの最も重要な概念であるコンポーネントについて学びましょう。

コンポーネントって何?

コンポーネントは、再利用可能なUIの部品です。 レゴブロックのように、小さな部品を組み合わせて大きなアプリケーションを作ります。

// ボタンコンポーネントの例
function Button({ text, onClick }) {
  return (
    <button 
      onClick={onClick}
      style={{
        backgroundColor: '#007bff',
        color: 'white',
        border: 'none',
        padding: '10px 20px',
        borderRadius: '5px',
        cursor: 'pointer'
      }}
    >
      {text}
    </button>
  );
}

// 使用例
function App() {
  return (
    <div>
      <Button text="保存" onClick={() => alert('保存しました')} />
      <Button text="キャンセル" onClick={() => alert('キャンセルしました')} />
      <Button text="削除" onClick={() => alert('削除しました')} />
    </div>
  );
}

一度作ったコンポーネントを何度でも使い回せます。

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

現在のReactでは、関数コンポーネントが主流です。

// 基本的な関数コンポーネント
function Greeting() {
  return <h1>こんにちは!</h1>;
}

// プロパティ(props)を受け取るコンポーネント
function PersonalGreeting({ name, age }) {
  return (
    <div>
      <h1>こんにちは、{name}さん!</h1>
      <p>年齢: {age}歳</p>
    </div>
  );
}

// デフォルト値を設定
function GreetingWithDefault({ name = 'ゲスト', age = 0 }) {
  return (
    <div>
      <h1>こんにちは、{name}さん!</h1>
      {age > 0 && <p>年齢: {age}歳</p>}
    </div>
  );
}

// 使用例
function App() {
  return (
    <div>
      <Greeting />
      <PersonalGreeting name="太郎" age={25} />
      <GreetingWithDefault name="花子" />
      <GreetingWithDefault />
    </div>
  );
}

関数として定義するだけで、コンポーネントが作れます。

プロパティ(Props)の活用

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

// ユーザーカードコンポーネント
function UserCard({ user, onEdit, onDelete }) {
  return (
    <div style={{
      border: '1px solid #ccc',
      borderRadius: '8px',
      padding: '16px',
      margin: '8px',
      backgroundColor: '#f9f9f9'
    }}>
      <h3>{user.name}</h3>
      <p>メール: {user.email}</p>
      <p>年齢: {user.age}歳</p>
      
      <div>
        <button 
          onClick={() => onEdit(user.id)}
          style={{ marginRight: '8px' }}
        >
          編集
        </button>
        <button 
          onClick={() => onDelete(user.id)}
          style={{ backgroundColor: '#dc3545', color: 'white' }}
        >
          削除
        </button>
      </div>
    </div>
  );
}

// ユーザーリストコンポーネント
function UserList() {
  const users = [
    { id: 1, name: '田中太郎', email: 'tanaka@example.com', age: 28 },
    { id: 2, name: '佐藤花子', email: 'sato@example.com', age: 32 },
    { id: 3, name: '鈴木一郎', email: 'suzuki@example.com', age: 25 }
  ];

  const handleEdit = (userId) => {
    alert(`ユーザー${userId}を編集します`);
  };

  const handleDelete = (userId) => {
    if (window.confirm('本当に削除しますか?')) {
      alert(`ユーザー${userId}を削除しました`);
    }
  };

  return (
    <div>
      <h2>ユーザー一覧</h2>
      {users.map(user => (
        <UserCard
          key={user.id}
          user={user}
          onEdit={handleEdit}
          onDelete={handleDelete}
        />
      ))}
    </div>
  );
}

Propsを使ってデータと機能を子コンポーネントに渡せます。

コンポーネントの分割と整理

大きなコンポーネントは、小さなコンポーネントに分割しましょう。

// ヘッダーコンポーネント
function Header({ title, userName }) {
  return (
    <header style={{
      backgroundColor: '#007bff',
      color: 'white',
      padding: '1rem',
      marginBottom: '1rem'
    }}>
      <h1>{title}</h1>
      {userName && <p>ようこそ、{userName}さん</p>}
    </header>
  );
}

// ナビゲーションコンポーネント
function Navigation({ currentPage, onPageChange }) {
  const pages = ['ホーム', 'ユーザー', '設定'];

  return (
    <nav style={{ marginBottom: '1rem' }}>
      {pages.map(page => (
        <button
          key={page}
          onClick={() => onPageChange(page)}
          style={{
            marginRight: '8px',
            backgroundColor: currentPage === page ? '#007bff' : '#f8f9fa',
            color: currentPage === page ? 'white' : '#333',
            border: '1px solid #ccc',
            padding: '8px 16px',
            borderRadius: '4px',
            cursor: 'pointer'
          }}
        >
          {page}
        </button>
      ))}
    </nav>
  );
}

// フッターコンポーネント
function Footer() {
  return (
    <footer style={{
      marginTop: '2rem',
      padding: '1rem',
      backgroundColor: '#f8f9fa',
      textAlign: 'center',
      borderTop: '1px solid #ccc'
    }}>
      <p>&copy; 2024 My React App. All rights reserved.</p>
    </footer>
  );
}

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

ファイル分割によるコンポーネント管理

プロジェクトが大きくなったら、コンポーネントを別ファイルに分割しましょう。

// components/Header.js
import React from 'react';

function Header({ title, userName }) {
  return (
    <header style={{
      backgroundColor: '#007bff',
      color: 'white',
      padding: '1rem',
      marginBottom: '1rem'
    }}>
      <h1>{title}</h1>
      {userName && <p>ようこそ、{userName}さん</p>}
    </header>
  );
}

export default Header;
// components/UserCard.js
import React from 'react';

function UserCard({ user, onEdit, onDelete }) {
  return (
    <div style={{
      border: '1px solid #ccc',
      borderRadius: '8px',
      padding: '16px',
      margin: '8px',
      backgroundColor: '#f9f9f9'
    }}>
      <h3>{user.name}</h3>
      <p>メール: {user.email}</p>
      <p>年齢: {user.age}歳</p>
      
      <div>
        <button 
          onClick={() => onEdit(user.id)}
          style={{ marginRight: '8px' }}
        >
          編集
        </button>
        <button 
          onClick={() => onDelete(user.id)}
          style={{ backgroundColor: '#dc3545', color: 'white' }}
        >
          削除
        </button>
      </div>
    </div>
  );
}

export default UserCard;
// App.js
import React, { useState } from 'react';
import Header from './components/Header';
import UserCard from './components/UserCard';

function App() {
  // アプリケーションのメインロジック
  return (
    <div>
      <Header title="My React App" userName="太郎" />
      {/* その他のコンポーネント */}
    </div>
  );
}

export default App;

ファイルを分けることで、コードの可読性と保守性が向上します。

コンポーネントを適切に設計することで、保守性が高く再利用可能なReactアプリケーションを構築できます。

State|データの管理と更新

Reactアプリケーションの動的な部分を実現するStateについて学びましょう。

Stateって何?

Stateは、コンポーネントが管理するデータです。 Stateが変更されると、コンポーネントが再描画され、UIが更新されます。

import { useState } from 'react';

function Counter() {
  // useState フックを使ってStateを定義
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>カウンター</h2>
      <p>現在の値: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        +1
      </button>
      <button onClick={() => setCount(count - 1)}>
        -1
      </button>
      <button onClick={() => setCount(0)}>
        リセット
      </button>
    </div>
  );
}

ボタンを押すと数値が変わり、画面も自動で更新されます。

useStateの基本的な使い方

useStateは、Reactで最も重要なフックの一つです。

import { useState } from 'react';

function StateExamples() {
  // 数値のState
  const [count, setCount] = useState(0);
  
  // 文字列のState
  const [name, setName] = useState('');
  
  // 真偽値のState
  const [isVisible, setIsVisible] = useState(true);
  
  // 配列のState
  const [items, setItems] = useState(['りんご', 'バナナ']);
  
  // オブジェクトのState
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  });

  return (
    <div>
      <h2>Stateの様々な例</h2>
      
      {/* 数値 */}
      <div>
        <h3>カウンター: {count}</h3>
        <button onClick={() => setCount(count + 1)}>増加</button>
      </div>
      
      {/* 文字列 */}
      <div>
        <h3>名前入力</h3>
        <input 
          value={name} 
          onChange={(e) => setName(e.target.value)}
          placeholder="名前を入力"
        />
        <p>入力された名前: {name}</p>
      </div>
      
      {/* 真偽値 */}
      <div>
        <button onClick={() => setIsVisible(!isVisible)}>
          {isVisible ? '非表示' : '表示'}
        </button>
        {isVisible && <p>この文章は表示/非表示が切り替わります</p>}
      </div>
    </div>
  );
}

様々なデータ型でStateを管理できます。

複雑なStateの管理

実際のアプリケーションでは、より複雑なStateを扱うことが多いです。

import { useState } from 'react';

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

  // 新しいTodoを追加
  const addTodo = () => {
    if (inputValue.trim()) {
      const newTodo = {
        id: Date.now(), // 簡易的なID生成
        text: inputValue.trim(),
        completed: false,
        createdAt: new Date()
      };
      setTodos([...todos, newTodo]);
      setInputValue('');
    }
  };

  // Todoの完了状態を切り替え
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id 
        ? { ...todo, completed: !todo.completed }
        : todo
    ));
  };

  // Todoを削除
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  // 完了したTodoをすべて削除
  const clearCompleted = () => {
    setTodos(todos.filter(todo => !todo.completed));
  };

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

  return (
    <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
      <h1>Todo アプリ</h1>
      
      {/* Todo追加フォーム */}
      <div style={{ marginBottom: '20px' }}>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
          placeholder="新しいタスクを入力..."
          style={{ 
            width: '70%', 
            padding: '10px', 
            marginRight: '10px',
            border: '1px solid #ccc',
            borderRadius: '4px'
          }}
        />
        <button 
          onClick={addTodo}
          style={{
            padding: '10px 20px',
            backgroundColor: '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer'
          }}
        >
          追加
        </button>
      </div>

      {/* フィルターボタン */}
      <div style={{ marginBottom: '20px' }}>
        {['all', 'active', 'completed'].map(filterType => (
          <button
            key={filterType}
            onClick={() => setFilter(filterType)}
            style={{
              marginRight: '10px',
              padding: '5px 15px',
              backgroundColor: filter === filterType ? '#007bff' : '#f8f9fa',
              color: filter === filterType ? 'white' : '#333',
              border: '1px solid #ccc',
              borderRadius: '4px',
              cursor: 'pointer'
            }}
          >
            {filterType === 'all' ? 'すべて' : 
             filterType === 'active' ? '未完了' : '完了'}
          </button>
        ))}
      </div>

      {/* Todoリスト */}
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {filteredTodos.map(todo => (
          <li 
            key={todo.id}
            style={{
              display: 'flex',
              alignItems: 'center',
              padding: '10px',
              marginBottom: '5px',
              backgroundColor: '#f9f9f9',
              borderRadius: '4px',
              border: '1px solid #eee'
            }}
          >
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
              style={{ marginRight: '10px' }}
            />
            <span 
              style={{ 
                flex: 1,
                textDecoration: todo.completed ? 'line-through' : 'none',
                color: todo.completed ? '#888' : '#333'
              }}
            >
              {todo.text}
            </span>
            <button
              onClick={() => deleteTodo(todo.id)}
              style={{
                backgroundColor: '#dc3545',
                color: 'white',
                border: 'none',
                borderRadius: '4px',
                padding: '5px 10px',
                cursor: 'pointer'
              }}
            >
              削除
            </button>
          </li>
        ))}
      </ul>

      {/* 統計とアクション */}
      <div style={{ marginTop: '20px', textAlign: 'center' }}>
        <p>
          全体: {todos.length}件 | 
          未完了: {todos.filter(t => !t.completed).length}件 | 
          完了: {todos.filter(t => t.completed).length}件
        </p>
        
        {todos.some(t => t.completed) && (
          <button
            onClick={clearCompleted}
            style={{
              backgroundColor: '#28a745',
              color: 'white',
              border: 'none',
              borderRadius: '4px',
              padding: '10px 20px',
              cursor: 'pointer'
            }}
          >
            完了したタスクを削除
          </button>
        )}
      </div>
    </div>
  );
}

このように複雑なデータも管理できます。

フォームでのState管理

フォームは、Stateを活用する典型的な例です。

import { useState } from 'react';

function UserForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    age: '',
    gender: '',
    interests: [],
    newsletter: false
  });

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

  // 入力値の変更を処理
  const handleInputChange = (e) => {
    const { name, value, type, checked } = e.target;
    
    if (type === 'checkbox') {
      if (name === 'interests') {
        // 複数選択チェックボックス
        const updatedInterests = checked
          ? [...formData.interests, value]
          : formData.interests.filter(interest => interest !== value);
        
        setFormData({
          ...formData,
          interests: updatedInterests
        });
      } else {
        // 単一チェックボックス
        setFormData({
          ...formData,
          [name]: checked
        });
      }
    } else {
      setFormData({
        ...formData,
        [name]: value
      });
    }
    
    // エラーをクリア
    if (errors[name]) {
      setErrors({
        ...errors,
        [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.age) {
      newErrors.age = '年齢は必須です';
    } else if (formData.age < 0 || formData.age > 120) {
      newErrors.age = '年齢は0-120の範囲で入力してください';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  // フォーム送信
  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (validateForm()) {
      console.log('送信データ:', formData);
      alert('フォームが送信されました!');
      
      // フォームをリセット
      setFormData({
        name: '',
        email: '',
        age: '',
        gender: '',
        interests: [],
        newsletter: false
      });
    }
  };

  return (
    <div style={{ maxWidth: '500px', margin: '0 auto', padding: '20px' }}>
      <h2>ユーザー登録フォーム</h2>
      
      <form onSubmit={handleSubmit}>
        {/* 名前 */}
        <div style={{ marginBottom: '15px' }}>
          <label>名前:</label>
          <input
            type="text"
            name="name"
            value={formData.name}
            onChange={handleInputChange}
            style={{
              width: '100%',
              padding: '8px',
              border: errors.name ? '2px solid red' : '1px solid #ccc',
              borderRadius: '4px'
            }}
          />
          {errors.name && <p style={{ color: 'red', fontSize: '14px' }}>{errors.name}</p>}
        </div>

        {/* メールアドレス */}
        <div style={{ marginBottom: '15px' }}>
          <label>メールアドレス:</label>
          <input
            type="email"
            name="email"
            value={formData.email}
            onChange={handleInputChange}
            style={{
              width: '100%',
              padding: '8px',
              border: errors.email ? '2px solid red' : '1px solid #ccc',
              borderRadius: '4px'
            }}
          />
          {errors.email && <p style={{ color: 'red', fontSize: '14px' }}>{errors.email}</p>}
        </div>

        <button 
          type="submit"
          style={{
            backgroundColor: '#007bff',
            color: 'white',
            border: 'none',
            padding: '12px 24px',
            borderRadius: '4px',
            cursor: 'pointer',
            width: '100%'
          }}
        >
          送信
        </button>
      </form>

      {/* デバッグ用:現在のフォーム状態を表示 */}
      <div style={{ marginTop: '20px', padding: '10px', backgroundColor: '#f8f9fa' }}>
        <h3>現在のフォーム状態:</h3>
        <pre>{JSON.stringify(formData, null, 2)}</pre>
      </div>
    </div>
  );
}

フォームの複雑な状態も簡単に管理できます。

Stateを適切に管理することで、ユーザーとのインタラクションが豊富な動的なWebアプリケーションを構築できます。

イベント処理|ユーザーとの対話

Reactでのイベント処理について、実践的な例とともに学びましょう。

基本的なイベント処理

Reactでは、HTMLとほぼ同じようにイベントを処理できます。 ただし、いくつか重要な違いがあります。

import { useState } from 'react';

function BasicEvents() {
  const [message, setMessage] = useState('');
  const [clickCount, setClickCount] = useState(0);

  // クリックイベント
  const handleClick = () => {
    setClickCount(clickCount + 1);
    setMessage(`ボタンが${clickCount + 1}回クリックされました`);
  };

  // マウスイベント
  const handleMouseEnter = () => {
    setMessage('マウスが要素の上に来ました');
  };

  const handleMouseLeave = () => {
    setMessage('マウスが要素から離れました');
  };

  // キーボードイベント
  const handleKeyPress = (e) => {
    if (e.key === 'Enter') {
      setMessage(`Enterキーが押されました: ${e.target.value}`);
    }
  };

  return (
    <div style={{ padding: '20px' }}>
      <h2>基本的なイベント処理</h2>
      
      {/* クリックイベント */}
      <button 
        onClick={handleClick}
        style={{
          padding: '10px 20px',
          marginRight: '10px',
          backgroundColor: '#007bff',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer'
        }}
      >
        クリック ({clickCount})
      </button>

      {/* マウスイベント */}
      <div
        onMouseEnter={handleMouseEnter}
        onMouseLeave={handleMouseLeave}
        style={{
          display: 'inline-block',
          padding: '10px 20px',
          backgroundColor: '#28a745',
          color: 'white',
          borderRadius: '4px',
          marginRight: '10px',
          cursor: 'pointer'
        }}
      >
        マウスを乗せてください
      </div>

      {/* キーボードイベント */}
      <input
        type="text"
        onKeyPress={handleKeyPress}
        placeholder="Enterキーを押してください"
        style={{
          padding: '10px',
          border: '1px solid #ccc',
          borderRadius: '4px'
        }}
      />

      {/* メッセージ表示 */}
      <div style={{ marginTop: '20px', padding: '10px', backgroundColor: '#f8f9fa' }}>
        <strong>メッセージ:</strong> {message}
      </div>
    </div>
  );
}

様々なイベントに反応できます。

フォームイベントの詳細処理

フォームでのイベント処理は、Webアプリケーションの核心部分です。

import { useState } from 'react';

function FormEvents() {
  const [formData, setFormData] = useState({
    username: '',
    password: '',
    email: ''
  });

  const [validationErrors, setValidationErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  // 入力変更イベント
  const handleInputChange = (e) => {
    const { name, value } = e.target;
    
    setFormData({
      ...formData,
      [name]: value
    });

    // リアルタイムバリデーション
    validateField(name, value);
  };

  // フィールド単位のバリデーション
  const validateField = (fieldName, value) => {
    const errors = { ...validationErrors };

    switch (fieldName) {
      case 'username':
        if (value.length < 3) {
          errors.username = 'ユーザー名は3文字以上で入力してください';
        } else {
          delete errors.username;
        }
        break;
      
      case 'password':
        if (value.length < 6) {
          errors.password = 'パスワードは6文字以上で入力してください';
        } else {
          delete errors.password;
        }
        break;
      
      case 'email':
        const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (!emailPattern.test(value)) {
          errors.email = '正しいメールアドレスを入力してください';
        } else {
          delete errors.email;
        }
        break;
      
      default:
        break;
    }

    setValidationErrors(errors);
  };

  // フォーカスイベント
  const handleFocus = (e) => {
    console.log(`${e.target.name} フィールドにフォーカスしました`);
  };

  // ブラーイベント(フィールドから離れた時)
  const handleBlur = (e) => {
    const { name, value } = e.target;
    console.log(`${name} フィールドからフォーカスが離れました`);
    validateField(name, value);
  };

  // フォーム送信イベント
  const handleSubmit = async (e) => {
    e.preventDefault(); // デフォルトの送信動作を防ぐ
    
    setIsSubmitting(true);

    // 全フィールドをバリデーション
    Object.keys(formData).forEach(field => {
      validateField(field, formData[field]);
    });

    // エラーがある場合は送信しない
    if (Object.keys(validationErrors).length > 0) {
      setIsSubmitting(false);
      return;
    }

    try {
      // 送信処理のシミュレーション
      await new Promise(resolve => setTimeout(resolve, 2000));
      
      alert('フォームが正常に送信されました!');
      
      // フォームをリセット
      setFormData({
        username: '',
        password: '',
        email: ''
      });
      
    } catch (error) {
      alert('送信中にエラーが発生しました');
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <div style={{ maxWidth: '400px', margin: '0 auto', padding: '20px' }}>
      <h2>フォームイベント処理</h2>
      
      <form onSubmit={handleSubmit}>
        {/* ユーザー名 */}
        <div style={{ marginBottom: '15px' }}>
          <label htmlFor="username">ユーザー名:</label>
          <input
            id="username"
            name="username"
            type="text"
            value={formData.username}
            onChange={handleInputChange}
            onFocus={handleFocus}
            onBlur={handleBlur}
            style={{
              width: '100%',
              padding: '8px',
              border: validationErrors.username ? '2px solid red' : '1px solid #ccc',
              borderRadius: '4px'
            }}
          />
          {validationErrors.username && (
            <p style={{ color: 'red', fontSize: '14px', margin: '5px 0' }}>
              {validationErrors.username}
            </p>
          )}
        </div>

        {/* 送信ボタン */}
        <button 
          type="submit"
          disabled={isSubmitting || Object.keys(validationErrors).length > 0}
          style={{
            width: '100%',
            padding: '12px',
            backgroundColor: isSubmitting ? '#6c757d' : '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: isSubmitting ? 'not-allowed' : 'pointer'
          }}
        >
          {isSubmitting ? '送信中...' : '送信'}
        </button>
      </form>
    </div>
  );
}

フォームの状態を細かく管理できます。

高度なイベント処理パターン

実際のアプリケーションでよく使われる高度なイベント処理パターンです。

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

function AdvancedEvents() {
  const [isDragging, setIsDragging] = useState(false);
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
  const [searchTerm, setSearchTerm] = useState('');
  const [timer, setTimer] = useState(null);
  
  const draggableRef = useRef(null);

  // ドラッグ&ドロップイベント
  const handleMouseDown = (e) => {
    setIsDragging(true);
    setDragStart({
      x: e.clientX - position.x,
      y: e.clientY - position.y
    });
  };

  const handleMouseMove = (e) => {
    if (isDragging) {
      setPosition({
        x: e.clientX - dragStart.x,
        y: e.clientY - dragStart.y
      });
    }
  };

  const handleMouseUp = () => {
    setIsDragging(false);
  };

  // グローバルマウスイベントの設定
  useEffect(() => {
    if (isDragging) {
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
    }

    return () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
  }, [isDragging, dragStart]);

  // デバウンス検索(入力から少し時間が経ってから検索実行)
  const handleSearchChange = (e) => {
    const value = e.target.value;
    setSearchTerm(value);

    // 前のタイマーをクリア
    if (timer) {
      clearTimeout(timer);
    }

    // 新しいタイマーを設定(500ms後に検索実行)
    const newTimer = setTimeout(() => {
      performSearch(value);
    }, 500);

    setTimer(newTimer);
  };

  const performSearch = (term) => {
    if (term.trim()) {
      console.log('検索実行:', term);
      // 実際の検索処理はここに記述
    }
  };

  return (
    <div style={{ padding: '20px', height: '500px', position: 'relative' }}>
      <h2>高度なイベント処理</h2>
      
      {/* 検索ボックス(デバウンス機能付き) */}
      <div style={{ marginBottom: '20px' }}>
        <label>検索(デバウンス機能付き):</label>
        <input
          type="text"
          value={searchTerm}
          onChange={handleSearchChange}
          placeholder="入力してから500ms後に検索実行"
          style={{
            width: '300px',
            padding: '8px',
            border: '1px solid #ccc',
            borderRadius: '4px',
            marginLeft: '10px'
          }}
        />
      </div>

      {/* ドラッグ可能な要素 */}
      <div
        ref={draggableRef}
        onMouseDown={handleMouseDown}
        style={{
          position: 'absolute',
          left: position.x,
          top: position.y + 100, // ヘッダー分のオフセット
          width: '100px',
          height: '100px',
          backgroundColor: isDragging ? '#007bff' : '#28a745',
          color: 'white',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          borderRadius: '8px',
          cursor: isDragging ? 'grabbing' : 'grab',
          userSelect: 'none',
          transition: isDragging ? 'none' : 'background-color 0.3s ease'
        }}
      >
        ドラッグ
      </div>
    </div>
  );
}

複雑なユーザーインタラクションも実装できます。

これらのイベント処理パターンを理解することで、ユーザーとの豊かな対話が可能なReactアプリケーションを構築できます。

実践プロジェクト|Todoアプリを作成

これまで学んだReactの知識を活用して、本格的なTodoアプリケーションを作成しましょう。

プロジェクトの設計

まず、Todoアプリの機能要件を整理します。

基本機能

  • Todoの追加
  • Todoの表示
  • Todoの完了/未完了切り替え
  • Todoの削除
  • フィルタリング(すべて/未完了/完了)
  • 残りタスク数の表示

追加機能

  • 編集機能
  • 優先度設定
  • 期限設定
  • データの永続化(localStorage)

ステップ1: プロジェクトのセットアップ

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

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

これで開発環境が整いました。

ステップ2: 基本的なTodoコンポーネント

// src/components/TodoItem.js
import React, { useState } from 'react';

function TodoItem({ todo, onToggle, onDelete, onEdit }) {
  const [isEditing, setIsEditing] = useState(false);
  const [editText, setEditText] = useState(todo.text);

  const handleEdit = () => {
    if (isEditing && editText.trim()) {
      onEdit(todo.id, editText.trim());
    }
    setIsEditing(!isEditing);
  };

  const handleKeyPress = (e) => {
    if (e.key === 'Enter') {
      handleEdit();
    } else if (e.key === 'Escape') {
      setEditText(todo.text);
      setIsEditing(false);
    }
  };

  const getPriorityColor = (priority) => {
    switch (priority) {
      case 'high': return '#dc3545';
      case 'medium': return '#ffc107';
      case 'low': return '#28a745';
      default: return '#6c757d';
    }
  };

  const isOverdue = todo.dueDate && new Date(todo.dueDate) < new Date() && !todo.completed;

  return (
    <li style={{
      display: 'flex',
      alignItems: 'center',
      padding: '12px',
      marginBottom: '8px',
      backgroundColor: todo.completed ? '#f8f9fa' : 'white',
      border: `1px solid ${isOverdue ? '#dc3545' : '#dee2e6'}`,
      borderRadius: '6px',
      opacity: todo.completed ? 0.7 : 1,
      transition: 'all 0.3s ease'
    }}>
      {/* 完了チェックボックス */}
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
        style={{ marginRight: '12px', transform: 'scale(1.2)' }}
      />

      {/* 優先度インジケーター */}
      <div style={{
        width: '4px',
        height: '40px',
        backgroundColor: getPriorityColor(todo.priority),
        marginRight: '12px',
        borderRadius: '2px'
      }} />

      {/* Todo内容 */}
      <div style={{ flex: 1 }}>
        {isEditing ? (
          <input
            type="text"
            value={editText}
            onChange={(e) => setEditText(e.target.value)}
            onKeyPress={handleKeyPress}
            onBlur={handleEdit}
            style={{
              width: '100%',
              padding: '4px 8px',
              border: '1px solid #ccc',
              borderRadius: '4px',
              fontSize: '16px'
            }}
            autoFocus
          />
        ) : (
          <div>
            <span style={{
              textDecoration: todo.completed ? 'line-through' : 'none',
              fontSize: '16px',
              color: todo.completed ? '#6c757d' : '#333'
            }}>
              {todo.text}
            </span>
            
            {/* 期限表示 */}
            {todo.dueDate && (
              <div style={{
                fontSize: '12px',
                color: isOverdue ? '#dc3545' : '#6c757d',
                marginTop: '4px'
              }}>
                期限: {new Date(todo.dueDate).toLocaleDateString()}
                {isOverdue && ' (期限切れ)'}
              </div>
            )}
            
            {/* 優先度ラベル */}
            <span style={{
              fontSize: '11px',
              padding: '2px 6px',
              backgroundColor: getPriorityColor(todo.priority),
              color: 'white',
              borderRadius: '10px',
              marginLeft: '8px'
            }}>
              {todo.priority === 'high' ? '高' : 
               todo.priority === 'medium' ? '中' : '低'}
            </span>
          </div>
        )}
      </div>

      {/* アクションボタン */}
      <div style={{ display: 'flex', gap: '8px' }}>
        <button
          onClick={handleEdit}
          style={{
            padding: '6px 12px',
            backgroundColor: isEditing ? '#28a745' : '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            fontSize: '12px'
          }}
        >
          {isEditing ? '保存' : '編集'}
        </button>
        
        <button
          onClick={() => onDelete(todo.id)}
          style={{
            padding: '6px 12px',
            backgroundColor: '#dc3545',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            fontSize: '12px'
          }}
        >
          削除
        </button>
      </div>
    </li>
  );
}

export default TodoItem;

このコンポーネントで、Todoアイテムの表示と編集ができます。

ステップ3: Todo追加フォーム

// src/components/TodoForm.js
import React, { useState } from 'react';

function TodoForm({ onAdd }) {
  const [text, setText] = useState('');
  const [priority, setPriority] = useState('medium');
  const [dueDate, setDueDate] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (text.trim()) {
      const newTodo = {
        id: Date.now(),
        text: text.trim(),
        completed: false,
        priority,
        dueDate: dueDate || null,
        createdAt: new Date().toISOString()
      };

      onAdd(newTodo);
      
      // フォームをリセット
      setText('');
      setPriority('medium');
      setDueDate('');
    }
  };

  return (
    <form 
      onSubmit={handleSubmit}
      style={{
        padding: '20px',
        backgroundColor: '#f8f9fa',
        borderRadius: '8px',
        marginBottom: '20px',
        border: '1px solid #dee2e6'
      }}
    >
      <h3 style={{ marginTop: 0 }}>新しいタスクを追加</h3>
      
      {/* タスク内容 */}
      <div style={{ marginBottom: '15px' }}>
        <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
          タスク内容:
        </label>
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
          placeholder="何をしますか?"
          style={{
            width: '100%',
            padding: '10px',
            border: '1px solid #ccc',
            borderRadius: '4px',
            fontSize: '16px'
          }}
          required
        />
      </div>

      <div style={{ display: 'flex', gap: '15px', marginBottom: '15px' }}>
        {/* 優先度 */}
        <div style={{ flex: 1 }}>
          <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
            優先度:
          </label>
          <select
            value={priority}
            onChange={(e) => setPriority(e.target.value)}
            style={{
              width: '100%',
              padding: '8px',
              border: '1px solid #ccc',
              borderRadius: '4px',
              fontSize: '14px'
            }}
          >
            <option value="low">低</option>
            <option value="medium">中</option>
            <option value="high">高</option>
          </select>
        </div>

        {/* 期限 */}
        <div style={{ flex: 1 }}>
          <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
            期限:
          </label>
          <input
            type="date"
            value={dueDate}
            onChange={(e) => setDueDate(e.target.value)}
            min={new Date().toISOString().split('T')[0]}
            style={{
              width: '100%',
              padding: '8px',
              border: '1px solid #ccc',
              borderRadius: '4px',
              fontSize: '14px'
            }}
          />
        </div>
      </div>

      {/* 送信ボタン */}
      <button
        type="submit"
        style={{
          width: '100%',
          padding: '12px',
          backgroundColor: '#007bff',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          fontSize: '16px',
          cursor: 'pointer',
          transition: 'background-color 0.3s ease'
        }}
      >
        タスクを追加
      </button>
    </form>
  );
}

export default TodoForm;

このフォームでTodoを追加できます。

ステップ4: メインアプリケーション

// src/App.js
import React, { useState, useEffect } from 'react';
import TodoForm from './components/TodoForm';
import TodoItem from './components/TodoItem';
import './App.css';

function App() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all');

  // localStorage からデータを読み込み
  useEffect(() => {
    const savedTodos = localStorage.getItem('todos');
    if (savedTodos) {
      try {
        setTodos(JSON.parse(savedTodos));
      } catch (error) {
        console.error('Failed to load todos:', error);
      }
    }
  }, []);

  // todos が変更されたら localStorage に保存
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  // Todo の追加
  const addTodo = (newTodo) => {
    setTodos([newTodo, ...todos]);
  };

  // Todo の完了状態を切り替え
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id 
        ? { ...todo, completed: !todo.completed }
        : todo
    ));
  };

  // Todo の削除
  const deleteTodo = (id) => {
    if (window.confirm('このタスクを削除しますか?')) {
      setTodos(todos.filter(todo => todo.id !== id));
    }
  };

  // Todo の編集
  const editTodo = (id, newText) => {
    setTodos(todos.map(todo =>
      todo.id === id 
        ? { ...todo, text: newText }
        : todo
    ));
  };

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

  // 統計情報
  const stats = {
    total: todos.length,
    active: todos.filter(todo => !todo.completed).length,
    completed: todos.filter(todo => todo.completed).length
  };

  return (
    <div style={{
      maxWidth: '800px',
      margin: '0 auto',
      padding: '20px',
      fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", sans-serif'
    }}>
      {/* ヘッダー */}
      <header style={{ 
        textAlign: 'center', 
        marginBottom: '30px',
        padding: '20px',
        background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
        color: 'white',
        borderRadius: '12px'
      }}>
        <h1 style={{ margin: 0, fontSize: '32px' }}>
          📝 Todo アプリ
        </h1>
        <p style={{ margin: '10px 0 0 0', opacity: 0.9 }}>
          効率的なタスク管理で生産性を向上
        </p>
      </header>

      {/* Todo 追加フォーム */}
      <TodoForm onAdd={addTodo} />

      {/* Todo リスト */}
      <div style={{
        backgroundColor: 'white',
        borderRadius: '8px',
        padding: '20px',
        border: '1px solid #dee2e6',
        minHeight: '200px'
      }}>
        {filteredTodos.length === 0 ? (
          <div style={{
            textAlign: 'center',
            padding: '40px',
            color: '#6c757d'
          }}>
            <div style={{ fontSize: '48px', marginBottom: '10px' }}>🎉</div>
            <p>タスクがありません。<br/>新しいタスクを追加してみましょう!</p>
          </div>
        ) : (
          <ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
            {filteredTodos.map(todo => (
              <TodoItem
                key={todo.id}
                todo={todo}
                onToggle={toggleTodo}
                onDelete={deleteTodo}
                onEdit={editTodo}
              />
            ))}
          </ul>
        )}
      </div>

      {/* フッター */}
      <footer style={{
        textAlign: 'center',
        marginTop: '40px',
        padding: '20px',
        color: '#6c757d',
        fontSize: '14px'
      }}>
        <p>データは自動的にブラウザに保存されます</p>
      </footer>
    </div>
  );
}

export default App;

これで完全に動作するTodoアプリが完成しました!

完成したTodoアプリの機能

このTodoアプリには以下の機能が実装されています:

  1. 基本機能

    • タスクの追加・編集・削除
    • 完了状態の切り替え
    • 期限設定と期限切れ表示
  2. 高度な機能

    • 優先度設定(高/中/低)
    • データの永続化(localStorage)
    • キーボード操作対応
  3. ユーザビリティ

    • 直感的なインターフェース
    • 確認ダイアログ
    • 視覚的なフィードバック

このプロジェクトを通じて、Reactの主要な概念をすべて実践的に学ぶことができました。

まとめ

Reactチュートリアルの完全ガイドを通じて、基礎から実践まで包括的に学習しました。

学習した重要なポイント

1. React の基本概念

  • コンポーネント思考による UI 構築
  • JSX による直感的な記述
  • 宣言的プログラミングの理解
  • Virtual DOM による効率的な更新

2. 実践的な開発スキル

  • useState による状態管理
  • イベント処理とユーザーとの対話
  • プロパティによるデータの受け渡し
  • 条件分岐とリスト表示

3. 本格的なアプリケーション開発

  • コンポーネントの設計と分割
  • フォームの実装とバリデーション
  • データの永続化
  • ユーザビリティの向上

React学習の成果

技術的スキル

  • React の核心概念の理解
  • JavaScript ES6+ の活用
  • モダンな開発手法の習得
  • 実践的なプログラミング能力

問題解決能力

  • コンポーネント思考による設計
  • 状態管理の適切な実装
  • ユーザーエクスペリエンスの考慮
  • 保守性の高いコード作成

次のステップ

さらなるスキル向上

  1. React Hooks の深い理解(useEffect、useContext など)
  2. 状態管理ライブラリ(Redux、Zustand)の学習
  3. React Router によるシングルページアプリケーション
  4. TypeScript との組み合わせ

実践的な開発

  • 個人プロジェクトの作成
  • オープンソースプロジェクトへの参加
  • ポートフォリオサイトの構築
  • 実際の業務での React 活用

継続的な学習

  • React 公式ドキュメントの定期的な確認
  • 新しい機能やベストプラクティスの習得
  • コミュニティでの情報交換
  • 実際のプロジェクトでの経験積み

学習のアドバイス

効果的な学習方法

  • 手を動かして実際にコードを書く
  • 小さなプロジェクトから始める
  • エラーを恐れずに挑戦する
  • 他の開発者のコードを読む

継続的な成長

  • 定期的な復習と実践
  • 新しい技術への挑戦
  • フィードバックの積極的な活用
  • 学習内容の整理とアウトプット

React は現代の Web 開発において最も重要な技術の一つです。 この記事で学んだ内容を基に、継続的に学習を進めて、優れた React 開発者を目指してください。

実際のプロジェクトでの経験を積むことで、より深い理解と実践的なスキルを身につけることができるでしょう。 React の世界は広く、学ぶべきことはまだたくさんありますが、この基盤があれば確実にステップアップできます。

頑張って、素晴らしい React アプリケーションを作成してください!

関連記事