Udemyセール!最大95%オフ!1,200円~Udemy公式サイト >

この記事にはプロモーションが含まれています。

【React】リスト表示と条件付きレンダリング|TODOアプリ【基本編:第5回】

【React】リスト表示と条件付きレンダリング|TODOアプリ【基本編:第5回】

Reactで作るTODOアプリ開発シリーズ第5回

この記事では

Reactでのリスト表示と条件付きレンダリングの実装方法

を学習していきます。

前回イベントハンドリングとフォーム処理について学習しました。今回は、データを動的に表示し、条件に応じてUIを制御する方法について学んでいきましょう。

リスト表示と条件付きレンダリングは、Reactアプリケーションで最もよく使われる機能の一つです。これらを適切に実装することで、ユーザーにとって使いやすく、見た目も美しいアプリケーションを作ることができます。

この記事でわかること
  • map関数を使ったリスト表示の基本
  • keyプロパティの重要性と使い方
  • 条件付きレンダリング(&&演算子、三項演算子)
  • TODOリストの表示とフィルタリング
  • 完了/未完了の切り替え表示
ケケンタ

リスト表示と条件付きレンダリングを理解することで、データに応じて動的に変化するUIを作ることができるようになります!

TODOアプリ(第5回:完成イメージ)その①
TODOアプリ(第5回:完成イメージ)その①
TODOアプリ(第5回:完成イメージ)その②
TODOアプリ(第5回:完成イメージ)その②
TODOアプリ(第5回:完成イメージ)その③
TODOアプリ(第5回:完成イメージ)その③
TODOアプリ(第5回:完成イメージ)その④
TODOアプリ(第5回:完成イメージ)その④

TODOアプリ開発シリーズのまとめ記事はこちら



ケケンタ

ケケンタのITブログでは、WebアプリPHPLaravel)やWeb制作WordPressコーディング)について情報を発信しています。
学習中の方や実務をされている方など多くの方にアクセスいただいていますので、ぜひほかの記事も参考にしてみてください!


運動不足、気になっていませんか?

もしプログラミング学習やお仕事で運動不足が気になっているなら

連続屈伸運動がおすすめです!

ボタンにカーソルを合わせるだけ
カウントダウンが始まるタイマーをご用意してみました!

ケケンタ

無理のない範囲で、ぜひ隙間時間に屈伸運動を取り入れてみて下さい!

タイマースタート

3:00

※運動不足だと連続3分で取り組んでもかなり息が切れます
(僕は加えて気分もちょっと悪くなりました……)
絶対にご無理の無い範囲でお取り組みください!



目次

前回までのコード

第4回で作成したコードを確認しましょう。以下のファイル構成になっています。

ファイル構造

todo-app/
├── src/
│   ├── App.js
│   ├── App.css
│   ├── TodoItem.js
│   ├── TodoList.js
│   ├── TodoHeader.js
│   └── TodoForm.js

App.js

import React, { useState } from 'react';
import './App.css';
import TodoHeader from './TodoHeader';
import TodoList from './TodoList';
import TodoForm from './TodoForm';

function App() {
  const [todos, setTodos] = useState([
    { id: 1, text: "Reactを学ぶ", completed: false, priority: "high" },
    { id: 2, text: "TODOアプリを作る", completed: true, priority: "medium" },
    { id: 3, text: "データベース連携", completed: false, priority: "low" },
    { id: 4, text: "デプロイする", completed: false, priority: "medium" }
  ]);

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

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

  // 新しいTODOを追加する関数
  const addTodo = (newTodo) => {
    setTodos(prevTodos => [...prevTodos, newTodo]);
  };

  return (
    <div className="App">
      <TodoHeader 
        title="React TODOアプリ"
        totalCount={todos.length}
        completedCount={completedCount}
      />
      <TodoForm onAddTodo={addTodo} />
      <TodoList todos={todos} onToggle={toggleTodo} />
    </div>
  );
}

export default App;

TodoForm.js

import React, { useState } from 'react';

function TodoForm({ onAddTodo }) {
  const [formData, setFormData] = useState({
    text: '',
    priority: 'medium'
  });
  const [error, setError] = useState('');

  const handleChange = (event) => {
    const { name, value } = event.target;
    setFormData(prevData => ({
      ...prevData,
      [name]: value
    }));

    if (error) {
      setError('');
    }
  };

  const handleSubmit = (event) => {
    event.preventDefault();

    if (!formData.text.trim()) {
      setError('TODOの内容を入力してください');
      return;
    }

    if (formData.text.length > 100) {
      setError('TODOの内容は100文字以下で入力してください');
      return;
    }

    const newTodo = {
      id: Date.now(),
      text: formData.text.trim(),
      completed: false,
      priority: formData.priority
    };

    onAddTodo(newTodo);

    setFormData({
      text: '',
      priority: 'medium'
    });
    setError('');
  };

  return (
    <form onSubmit={handleSubmit} className="todo-form">
      <div className="form-group">
        <input
          type="text"
          name="text"
          value={formData.text}
          onChange={handleChange}
          placeholder="新しいTODOを入力してください"
          className="todo-input"
        />
        <select
          name="priority"
          value={formData.priority}
          onChange={handleChange}
          className="priority-select"
        >
          <option value="low">低優先度</option>
          <option value="medium">中優先度</option>
          <option value="high">高優先度</option>
        </select>
        <button type="submit" className="add-button">
          追加
        </button>
      </div>
      {error && <p className="error-message">{error}</p>}
    </form>
  );
}

export default TodoForm;

App.css

.todo-form {
  margin-bottom: 30px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
}

.form-group {
  display: flex;
  gap: 10px;
  align-items: center;
}

.todo-input {
  flex: 1;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
}

.todo-input:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

.priority-select {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
  background-color: white;
}

.add-button {
  padding: 10px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.add-button:hover {
  background-color: #0056b3;
}

.add-button:disabled {
  background-color: #6c757d;
  cursor: not-allowed;
}

.error-message {
  color: #dc3545;
  margin-top: 10px;
  font-size: 14px;
}

リスト表示の基本

実際に手を動かす前に、まずは必要な知識について解説をしていきます。

Reactで配列のデータを表示するには、map関数を使用します。map関数は、配列の各要素に対して処理を行い、新しい配列を返します。

基本的なリスト表示

function BasicList() {
  const items = ["りんご", "みかん", "バナナ", "ぶどう"];

  return (
    <div>
      <h3>果物リスト</h3>
      <ul>
        {items.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

オブジェクトのリスト表示

function ObjectList() {
  const users = [
    { id: 1, name: "田中太郎", age: 25 },
    { id: 2, name: "佐藤花子", age: 30 },
    { id: 3, name: "鈴木一郎", age: 28 }
  ];

  return (
    <div>
      <h3>ユーザーリスト</h3>
      <ul>
        {users.map(user => (
          <li key={user.id}>
            {user.name} ({user.age}歳)
          </li>
        ))}
      </ul>
    </div>
  );
}

keyプロパティの重要性

Reactでリストを表示する際、各要素に一意のkeyプロパティを指定する必要があります。これは、Reactが要素を効率的に更新するために使用されます。

keyプロパティの基本

function KeyExample() {
  const [items, setItems] = useState([
    { id: 1, text: "アイテム1" },
    { id: 2, text: "アイテム2" },
    { id: 3, text: "アイテム3" }
  ]);

  const addItem = () => {
    const newItem = {
      id: Date.now(),
      text: `アイテム${items.length + 1}`
    };
    setItems(prevItems => [...prevItems, newItem]);
  };

  return (
    <div>
      <button onClick={addItem}>アイテムを追加</button>
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    </div>
  );
}

keyプロパティの注意点

// ❌ 避けるべき書き方(indexをkeyとして使用)
function BadKeyExample() {
  const items = ["りんご", "みかん", "バナナ"];

  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item}</li> // 配列の順序が変わる可能性がある場合は危険
      ))}
    </ul>
  );
}

// ✅ 推奨される書き方(一意のIDを使用)
function GoodKeyExample() {
  const items = [
    { id: 1, name: "りんご" },
    { id: 2, name: "みかん" },
    { id: 3, name: "バナナ" }
  ];

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

条件付きレンダリング

条件付きレンダリングは、特定の条件に基づいて異なるコンテンツを表示する機能です。

基本的な条件付きレンダリング

1. &&演算子を使用

function ConditionalRendering() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [userName, setUserName] = useState("");

  return (
    <div>
      {isLoggedIn && (
        <div>
          <h2>ようこそ、{userName}さん!</h2>
          <button onClick={() => setIsLoggedIn(false)}>
            ログアウト
          </button>
        </div>
      )}

      {!isLoggedIn && (
        <div>
          <h2>ログインしてください</h2>
          <input
            type="text"
            placeholder="ユーザー名"
            value={userName}
            onChange={(e) => setUserName(e.target.value)}
          />
          <button onClick={() => setIsLoggedIn(true)}>
            ログイン
          </button>
        </div>
      )}
    </div>
  );
}

2. 三項演算子を使用

function TernaryExample() {
  const [isLoading, setIsLoading] = useState(true);
  const [data, setData] = useState(null);

  return (
    <div>
      {isLoading ? (
        <div>読み込み中...</div>
      ) : (
        <div>
          <h3>データが読み込まれました</h3>
          <p>{data}</p>
        </div>
      )}
    </div>
  );
}

3. 複数の条件分岐

function MultipleConditions() {
  const [status, setStatus] = useState("loading");

  const renderContent = () => {
    switch (status) {
      case "loading":
        return <div>読み込み中...</div>;
      case "success":
        return <div>成功しました!</div>;
      case "error":
        return <div>エラーが発生しました</div>;
      default:
        return <div>不明な状態です</div>;
    }
  };

  return (
    <div>
      <div>現在の状態: {status}</div>
      {renderContent()}
      <button onClick={() => setStatus("success")}>
        成功にする
      </button>
      <button onClick={() => setStatus("error")}>
        エラーにする
      </button>
    </div>
  );
}

リスト表示と条件付きレンダリングの組み合わせ

実際のアプリケーションでは、リスト表示と条件付きレンダリングを組み合わせて使用することが多いです。

フィルタリング機能の実装

function FilteredList() {
  const [filter, setFilter] = useState("all");
  const [items, setItems] = useState([
    { id: 1, text: "買い物に行く", completed: false },
    { id: 2, text: "本を読む", completed: true },
    { id: 3, text: "運動する", completed: false },
    { id: 4, text: "料理を作る", completed: true }
  ]);

  const filteredItems = items.filter(item => {
    if (filter === "all") return true;
    if (filter === "completed") return item.completed;
    if (filter === "pending") return !item.completed;
    return true;
  });

  return (
    <div>
      <div>
        <button onClick={() => setFilter("all")}>
          すべて
        </button>
        <button onClick={() => setFilter("completed")}>
          完了済み
        </button>
        <button onClick={() => setFilter("pending")}>
          未完了
        </button>
      </div>

      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>
            <span style={{ 
              textDecoration: item.completed ? 'line-through' : 'none' 
            }}>
              {item.text}
            </span>
            {item.completed && <span> ✓</span>}
          </li>
        ))}
      </ul>

      {filteredItems.length === 0 && (
        <p>該当するアイテムがありません</p>
      )}
    </div>
  );
}

【実践】フィルタリング機能と条件付きレンダリングの実装

ここからは実際に手を動かして、TODOアプリにフィルタリング機能と条件付きレンダリングを追加していきましょう。

1. TodoFilterコンポーネントの作成

src/TodoFilter.jsファイルを新規作成し、以下のコードを記述してください。

import React from 'react';

function TodoFilter({ filter, onFilterChange }) {
  return (
    <div className="todo-filter">
      <button
        className={`filter-button ${filter === 'all' ? 'active' : ''}`}
        onClick={() => onFilterChange('all')}
      >
        すべて
      </button>
      <button
        className={`filter-button ${filter === 'pending' ? 'active' : ''}`}
        onClick={() => onFilterChange('pending')}
      >
        未完了
      </button>
      <button
        className={`filter-button ${filter === 'completed' ? 'active' : ''}`}
        onClick={() => onFilterChange('completed')}
      >
        完了済み
      </button>
    </div>
  );
}

export default TodoFilter;

2. App.jsの修正

既存のsrc/App.jsを以下のように修正してください。

import React, { useState } from 'react';
import './App.css';
import TodoHeader from './TodoHeader';
import TodoList from './TodoList';
import TodoForm from './TodoForm';
import TodoFilter from './TodoFilter';

function App() {
  const [todos, setTodos] = useState([
    { id: 1, text: "Reactを学ぶ", completed: false, priority: "high" },
    { id: 2, text: "TODOアプリを作る", completed: true, priority: "medium" },
    { id: 3, text: "データベース連携", completed: false, priority: "low" },
    { id: 4, text: "デプロイする", completed: false, priority: "medium" }
  ]);

  const [filter, setFilter] = useState('all');

  // フィルタリングされたTODOリスト
  const filteredTodos = todos.filter(todo => {
    if (filter === 'all') return true;
    if (filter === 'completed') return todo.completed;
    if (filter === 'pending') return !todo.completed;
    return true;
  });

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

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

  // 新しいTODOを追加する関数
  const addTodo = (newTodo) => {
    setTodos(prevTodos => [...prevTodos, newTodo]);
  };

  return (
    <div className="App">
      <TodoHeader 
        title="React TODOアプリ"
        totalCount={todos.length}
        completedCount={completedCount}
      />
      <TodoForm onAddTodo={addTodo} />
      <TodoFilter filter={filter} onFilterChange={setFilter} />
      <TodoList todos={filteredTodos} onToggle={toggleTodo} />
    </div>
  );
}

export default App;

3. TodoList.jsの修正

既存のsrc/TodoList.jsを以下のように修正してください。

import React from 'react';
import TodoItem from './TodoItem';

function TodoList({ todos, onToggle }) {
  // TODOが存在しない場合の表示
  if (todos.length === 0) {
    return (
      <div className="empty-state">
        <p>TODOがありません。新しいTODOを追加してください。</p>
      </div>
    );
  }

  return (
    <div className="todo-list">
      {todos.map((todo) => (
        <TodoItem 
          key={todo.id}
          id={todo.id}
          text={todo.text}
          completed={todo.completed}
          priority={todo.priority}
          onToggle={onToggle}
        />
      ))}
    </div>
  );
}

export default TodoList;

4. TodoItem.jsの修正

既存のsrc/TodoItem.jsを以下のように修正してください。

import React from 'react';

function TodoItem({ id, text, completed, priority = "medium", onToggle }) {
  return (
    <div className={`todo-item priority-${priority}`}>
      <input 
        type="checkbox" 
        checked={completed} 
        onChange={() => onToggle(id)}
      />
      <span style={{ 
        textDecoration: completed ? 'line-through' : 'none' 
      }}>
        {text}
      </span>
      {completed && (
        <span className="completed-badge">完了</span>
      )}
      {!completed && (
        <span className="pending-badge">未完了</span>
      )}
    </div>
  );
}

export default TodoItem;

5. スタイルの追加

既存のsrc/App.cssに以下のスタイルを追加してください。

.todo-filter {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-bottom: 20px;
}

.filter-button {
  padding: 8px 16px;
  border: 1px solid #ddd;
  background-color: white;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s;
}

.filter-button:hover {
  background-color: #f8f9fa;
}

.filter-button.active {
  background-color: #007bff;
  color: white;
  border-color: #007bff;
}

.empty-state {
  text-align: center;
  padding: 40px;
  color: #666;
  font-style: italic;
}

.completed-badge {
  background-color: #28a745;
  color: white;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
  margin-left: 10px;
}

.pending-badge {
  background-color: #ffc107;
  color: #212529;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
  margin-left: 10px;
}

動作確認

実装が完了したら、開発サーバーが起動している状態でブラウザを確認してください。以下の機能が動作することを確認できます。

TODOアプリ(第5回:完成イメージ)その①
TODOアプリ(第5回:完成イメージ)その①
TODOアプリ(第5回:完成イメージ)その②
TODOアプリ(第5回:完成イメージ)その②
TODOアプリ(第5回:完成イメージ)その③
TODOアプリ(第5回:完成イメージ)その③
TODOアプリ(第5回:完成イメージ)その④
TODOアプリ(第5回:完成イメージ)その④
  • フィルターボタンでTODOリストを絞り込み表示
  • 完了/未完了のバッジ表示
  • TODOが存在しない場合のメッセージ表示
  • アクティブなフィルターボタンのハイライト表示

FAQ

map関数でエラーが発生する場合

以下の点を確認してください。

  1. 配列が正しく定義されているか
  2. map関数の構文が正しいか
  3. 配列の要素が存在するか(空配列の場合は条件分岐を追加)
keyプロパティの警告が表示される場合

以下の点を確認してください。

  1. 各要素に一意のkeyプロパティが設定されているか
  2. keyの値が安定しているか(配列のインデックスは避ける)
  3. keyの値が重複していないか
条件付きレンダリングが動作しない場合

以下の点を確認してください。

  1. 条件式が正しく記述されているか
  2. 条件の値が期待する型になっているか
  3. 条件に応じた要素が正しく返されているか
フィルタリングが動作しない場合

以下の点を確認してください。

  1. フィルター条件が正しく設定されているか
  2. filter関数の構文が正しいか
  3. フィルター状態が正しく更新されているか
空の状態表示が動作しない場合

以下の点を確認してください。

  1. 配列の長さチェックが正しく記述されているか
  2. 空の状態用のコンポーネントが正しく定義されているか
  3. 条件分岐の順序が適切か
バッジやスタイルが表示されない場合

以下の点を確認してください。

  1. CSSクラス名が正しく記述されているか
  2. 条件付きレンダリングの構文が正しいか
  3. CSSファイルが正しくimportされているか

まとめ

今回は、Reactでのリスト表示と条件付きレンダリングについて学習しました。

map関数を使ったリスト表示により、データに応じて動的にUIを生成することができるようになりました。また、keyプロパティの重要性を理解し、効率的なリスト更新の方法を学びました。

条件付きレンダリングにより、ユーザーの操作やデータの状態に応じて適切なUIを表示することができるようになりました。

ケケンタ

次回は、TODOの編集と削除機能について学習していきます。CRUD操作の実装を通じて、より実践的なアプリケーション開発を体験していきましょう!

この記事が少しでもお役に立ったなら何よりです。次回もお楽しみに!


僕が実際にReact学習に使用した書籍です

【React】リスト表示と条件付きレンダリング|TODOアプリ【基本編:第5回】のアイキャッチ画像

この記事が気に入ったら
フォローしてね!

この記事が良いと思ったらシェアしてね!

コメント

コメントする

CAPTCHA


目次