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

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

【React】編集・削除機能の実装|TODOアプリ【基本編:第6回】

【React】編集・削除機能の実装|TODOアプリ【基本編:第6回】

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

この記事では、

「編集」と「削除」機能

を追加し、基本的なCRUD操作を完成させます。

前回はリスト表示と条件付きレンダリングについて学習しました。今回は、ユーザーがTODOを編集・削除できるようにし、より実用的なアプリに仕上げていきます。

この記事でわかること
  • 配列のfilter/mapによるデータ操作
  • 編集・削除機能の実装方法
  • 編集モードの切り替え(条件付きレンダリング)
  • キーボードイベントの活用
  • UI/UX向上のための工夫
ケケンタ

編集・削除機能の実装方法を理解することで、CRUD操作を含めたReactアプリの開発が行えるようになります!

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

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



ケケンタ

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


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

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

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

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

ケケンタ

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

タイマースタート

3:00

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



目次

前回までのコード

第5回まで作成・修正したファイル構成と主要なコードは以下の通りです。

ファイル構成

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

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;

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;

TodoList.js

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

function TodoList({ todos, onToggle }) {
  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;

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;

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;
}

編集・削除機能の実装に必要な前提知識

編集・削除機能を実装する前に、必要な前提知識を整理しておきましょう。

配列操作の基礎

編集・削除機能では、配列のfiltermapメソッドを活用します。

filterメソッド

配列から条件に合う要素のみを抽出するメソッドです。

// 削除機能で使用:指定したID以外のTODOを残す
const newTodos = todos.filter(todo => todo.id !== idToDelete);

mapメソッド

配列の各要素を変換するメソッドです。

// 編集機能で使用:指定したIDのTODOのテキストを更新
const newTodos = todos.map(todo =>
  todo.id === idToEdit
    ? { ...todo, text: newText }
    : todo
);

イベントの伝播と停止

編集モードでは、キーボードイベント(Enter、Escape)を活用します。

これにより、ユーザーはマウスを使用しなくてもTODOの追加・編集・キャンセルができるようになるため、ユーザビリティの向上につながります。

onKeyDownイベント

キーが押された瞬間に発火するイベントです。

const handleKeyDown = (event) => {
  if (event.key === 'Enter') {
    // 保存処理
  }
  if (event.key === 'Escape') {
    // キャンセル処理
  }
};

条件付きレンダリングの応用

編集モードと表示モードを切り替えるために、条件付きレンダリングを活用します。

{isEditing ? (
  // 編集モードのUI
) : (
  // 表示モードのUI
)}

【実践】編集・削除機能の実装

ここからは、実際に手を動かして、TODOの編集・削除機能を追加していきましょう。

1. TodoItem.jsの修正

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

import React, { useState } from 'react';

function TodoItem({ id, text, completed, priority = "medium", onToggle, onDelete, onEdit }) {
  const [isEditing, setIsEditing] = useState(false);
  const [editText, setEditText] = useState(text);

  const handleEdit = () => {
    setIsEditing(true);
    setEditText(text);
  };

  const handleSave = () => {
    if (editText.trim()) {
      onEdit(id, editText);
      setIsEditing(false);
    }
  };

  const handleCancel = () => {
    setIsEditing(false);
    setEditText(text);
  };

  const handleKeyDown = (event) => {
    if (event.key === 'Enter') {
      handleSave();
    }
    if (event.key === 'Escape') {
      handleCancel();
    }
  };

  return (
    <div className={`todo-item priority-${priority}`}>
      <input
        type="checkbox"
        checked={completed}
        onChange={() => onToggle(id)}
      />
      {isEditing ? (
        <div className="edit-section">
          <input
            type="text"
            value={editText}
            onChange={(e) => setEditText(e.target.value)}
            onKeyDown={handleKeyDown}
            className="edit-input"
            autoFocus
          />
          <button onClick={handleSave} className="save-button">保存</button>
          <button onClick={handleCancel} className="cancel-button">キャンセル</button>
        </div>
      ) : (
        <div className="todo-content">
          <span style={{ textDecoration: completed ? 'line-through' : 'none' }}>{text}</span>
          <span className={`priority-badge ${priority}`}>{priority === 'high' ? '高' : priority === 'medium' ? '中' : '低'}</span>
          <div className="todo-actions">
            <button onClick={handleEdit} className="edit-button">編集</button>
            <button onClick={() => onDelete(id)} className="delete-button">削除</button>
          </div>
        </div>
      )}
    </div>
  );
}

export default TodoItem;

2. TodoList.jsの修正

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

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

function TodoList({ todos, onToggle, onDelete, onEdit }) {
  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}
          onDelete={onDelete}
          onEdit={onEdit}
        />
      ))}
    </div>
  );
}

export default TodoList;

3. 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');

  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;

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

  const addTodo = (newTodo) => {
    setTodos(prevTodos => [...prevTodos, newTodo]);
  };

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

  const editTodo = (id, newText) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id
          ? { ...todo, text: newText }
          : todo
      )
    );
  };

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

export default App;

4. App.cssの追加スタイル

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

.edit-section {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
}
.edit-input {
  flex: 1;
  padding: 8px 12px;
  border: 2px solid #007bff;
  border-radius: 4px;
  font-size: 14px;
  outline: none;
}
.save-button {
  padding: 6px 12px;
  background-color: #28a745;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}
.save-button:hover {
  background-color: #218838;
}
.cancel-button {
  padding: 6px 12px;
  background-color: #6c757d;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}
.cancel-button:hover {
  background-color: #5a6268;
}
.todo-actions {
  display: flex;
  gap: 8px;
  margin-left: auto;
}
.edit-button {
  padding: 4px 8px;
  background-color: #ffc107;
  color: #212529;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}
.edit-button:hover {
  background-color: #e0a800;
}
.delete-button {
  padding: 4px 8px;
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}
.delete-button:hover {
  background-color: #c82333;
}

動作確認

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

TODOアプリ(第6回:完成イメージ)その①
TODOアプリ(第6回:完成イメージ)その①
TODOアプリ(第6回:完成イメージ)その②
TODOアプリ(第6回:完成イメージ)その②
TODOアプリ(第6回:完成イメージ)その③
TODOアプリ(第6回:完成イメージ)その③
  • 編集ボタンで編集モードに切り替え、保存・キャンセルができる
  • 削除ボタンでTODOが削除できる
  • Enterキーで保存、Escapeキーでキャンセル
  • 編集・削除後もフィルタリングが正常に動作する

発展課題

基本機能を理解したら、ぜひ以下の課題に挑戦してみてください!

  1. 削除確認ダイアログの実装
  2. 一括削除機能の追加
  3. 編集履歴の保存・元に戻す機能

FAQ

削除機能でエラーが発生する場合

onDelete関数が正しくpropsで渡されているか、filter関数の構文やIDの比較が正しいか確認してください。

編集機能でエラーが発生する場合

編集状態やテキストの状態管理、保存・キャンセル処理が正しく実装されているか確認してください。

編集モードの切り替えが動作しない場合

条件付きレンダリングの条件式や編集状態の初期値、切り替え処理が正しいか確認してください。

キーボードショートカットが動作しない場合

onKeyDownイベントハンドラーやキーコードの判定、イベント伝播の制御が正しいか確認してください。

編集後のデータが正しく更新されない場合

map関数やスプレッド演算子の使い方、状態更新関数の呼び出しが正しいか確認してください。

削除後のフィルタリングが正しく動作しない場合

削除後にフィルター条件が正しく適用されているか、フィルタリング関数や状態の更新タイミングが適切か確認してください。

編集時のバリデーションが動作しない場合

空文字チェックやバリデーション条件、エラーメッセージの表示が正しいか確認してください。

編集キャンセル時に元の値に戻らない場合

キャンセル処理や編集状態のリセット、初期値の保存が正しいか確認してください。

まとめ

今回は、React TODOアプリに編集・削除機能を追加し、基本的なCRUD操作を完成させました。

  • filterメソッドによる削除
  • mapメソッドによる編集
  • 条件付きレンダリングによる編集モードの切り替え
  • キーボードイベントの活用
  • 状態管理の応用

ここまでお疲れさまでした! 次回はいよいよ「基本編」の最終回です!

ケケンタ

基本編の最後では、ローカルストレージを使用してデータを永続化させます。これによりブラウザを更新してもTODOリストの内容がリセットされないようになります!

データベースを使用せず簡単に実装できる内容です。ローカルストレージを使用できれば実装できることの幅が大きく広がります。次回もどうぞお楽しみに!


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

【React】編集・削除機能の実装|TODOアプリ【基本編:第6回】のアイキャッチ画像

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

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

コメント

コメントする

CAPTCHA


目次