ReactでTodoアプリケーションを作ってみました



React の基礎を学ぶために公式のチュートリアルはやってみたのですが、他にも何か作って理解を深めたいと思い Todo アプリケーションを作ることにしました。
参考にできそうなサイトを探すと出てはくるのですが、どういった順番で作成するのかがわからない記事が多かったため、動画を参考にさせていただき作成しました。





*作るもの

Todoタスクの追加、削除、編集する機能を実装しました。チェックを付けたタスクを「完了」とし、一括で全て完了/未完了にしたり、完了にしたタスクを一括削除する機能も付けました。また、タスクを完了/未完了/全てでフィルタリングして表示できるようにもしてあります。
React の基礎的な学習をすることを目的としているので、レイアウトは適当です。












*目次

  • プロジェクト作成
  • 準備
  • モックを作成
  • Formコンポーネントの作成
  • Stateを追加
  • Todoコンポーネントの作成
  • タスク追加機能の実装
  • 完了チェックボックスの実装
  • 一括チェックボックスの実装
  • 完了タスク一括削除ボタンの実装
  • フィルター機能の実装
  • 削除機能を実装
  • 編集機能の実装


*参考



*環境

  • MacOS
  • create-react-app 3.0.1
  • react 16.8.6
  • react-dom 16.8.6
  • react-scripts 3.0.1
  • node 12.4.0


*プロジェクト作成

create-react-appを使ってプロジェクトの雛形を作成します。
インストール方法については下記の記事を参照してください。
create-react-appを実行し、作成されたディレクトリの中に入ります。
$ create-react-app react-todo
$ cd react-todo/

ディレクトリ構成は下記になります。
react-todo
├── README.md
├── node_modules
├── package-lock.json
├── package.json
├── public
└── src
 ├── App.css
 ├── App.js
 ├── App.test.js
 ├── index.css
 ├── index.js
 ├── logo.svg
 └── serviceWorker.js

src/index.jsが React アプリのエントリーポイントになります。(アプリケーション起動時に最初に動くファイル)public/index.html<div id="root"></div>があり、src/index.jsがそのdiv要素を取得して React コンポーネントのAppとしてマウントしています。このpublic/index.htmlが、React アプリがレンダリングされるときの本体になります。

src/index.jsでインポートされているsrc/App.jsは、雛形作成直後にアプリケーションを起動すると表示されるレイアウトが書かれています。
<src/index.js>
// Reactコンポーネントをマウント
ReactDOM.render(<App  />,  document.getElementById('root'));

補足ですが、開発用サーバーでアプリケーションを起動したい場合は下記コマンドを使います。
// 開発用サーバーで起動
$ npm run start

// 本番用にビルド
$ npm run build

また、Chrome拡張の「React Developer Tools」を入れるとブラウザでReactのソースコードの確認ができて便利です。


*準備

srcディレクトリ配下にある7ファイルを全部削除し、src/index.jsを新規作成します。
react-domはDOM要素を変換するために使います。react-domが持っているrender()にDOM要素と表示する場所を指定して使います。
index.jsを下記実装にすると画面に「hoge」が表示されます。
<src/index.js>
import React from 'react'
import ReactDOM from 'react-dom'

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

componentsディレクトリを作成してApp.jsを新規作成します。
下記では class コンポーネントを使って書いていますが、function コンポーネントで書くこともできます。
<src/components/App.js>
import React from 'react';

class App extends React.Component {
  render() {
    return <div>hoge</div>;
  };
}

export default App;

上記では class コンポーネントを使って書いていますが、function コンポーネントで書くこともできます。
<src/components/App.js>
import React from 'react';

const App = () => <div>hoge</div>;

export default App;

src/index.jsを修正します。
作成したAppコンポーネントを読み込み、render()の引数のDOM要素だった部分を<App />に置き換えます。
画面での表示は変わらず「hoge」が表示されます。
<src/index.js>
import React from 'react'
import ReactDOM from 'react-dom'

import App from './components/App';

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


*モックを作成

App.jsにレイアウトを追加します。
returnにカッコを追加し、DOM要素を書いていきます。まずは動かなくていいのでサンプルデータを使って全レイアウトを作成します。
<src/components/App.js>
import React from 'react';

class App extends React.Component {
  render() {
    return (
      <div>
        <form>
          <input type="text" />
          <button>追加</button>
        </form>

        <label>
          <input type="checkbox" />
          全て完了にする
        </label>

        <select>
          <option>全て</option>
          <option>未完了</option>
          <option>完了済み</option>
        </select>

        <ul>
          <li>
            <label>
              <input type="checkbox" />
              朝ごはんを食べる
            </label>
            <button>編集</button>
            <button>削除</button>
          </li>
          <li>
            <label>
              <input type="checkbox" />
              筋トレをする
            </label>
            <button>編集</button>
            <button>削除</button>
          </li>
          <li>
            <label>
              <input type="checkbox" />
              勉強をする
            </label>
            <button>編集</button>
            <button>削除</button>
          </li>
        </ul>
        <button>完了済みを全て削除</button>
      </div>
    )
  };
}

export default App;












*Formコンポーネントの作成

App.jsに書いた追加ボタンの Form タグをコンポーネント化します。
components/Form.jsを新規作成します。
returnの中に書くDOM要素はApp.jsからコピーします。
<src/components/Form.js>
import React from 'react';

class Form extends React.Component {
  render() {
    return (
      <form>
        <input type="text" />
        <button>追加</button>
      </form>
    )
  };
}

export default Form;

App.jsを修正します。
form タグを Form コンポーネントに置き換えます。
画面に表示される情報は今までと同じ状態です。
<src/components/App.js>
import React from 'react';

// ----- 追加 -----
import Form from  './Form';

class App extends React.Component {
  render() {
    return (
      <div>
        // ----- Formに修正 -----
        <Form />
        <label>
          <input type="checkbox" />
          全て完了にする
        </label>
...


*Stateを追加

入力したタスクを保存できるよう React のstateという機能を使って状態を管理します。
Form.jsconstructor()を追加してthis.stateを定義します。super()React.Componentを呼び出していることを意味しています。

次にhandleChange()を追加し、this.setState()で入力されたタスク名を保存します。eはイベントを意味しています。入力されたイベントが発生したときにhandleChange()を呼び出したいため、inputタグにonChangeを追加してhandleChange()を指定します。

処理の流れは下記になります。
  1. 入力されたらonChangeイベントが発生し、handleChange()が呼ばれる。
  2. setState()stateを更新
  3. state.inputの値が入力フォームに表示
<src/components/Form.js>
import React from 'react';

class Form extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      input: ''
    };
  }
  render() {
    return (
      <form>
        <input type="text" value={this.state.input} onChange={this.handleChange} />
        <button>追加</button>
      </form>
    );
  }
  handleChange = e => {
    this.setState({ input: e.currentTarget.value })
  }
}

export default Form;


*Todoコンポーネントの作成

1つのタスクが表示される部分をコンポーネント化します。
components/Todo.jsを新規作成します。
render()するときの DOM は1つでなければいけないので、親要素として div タグで囲ってひとまとまりにする必要があります。
タスクは複数登録されるので、汎用的に使えるよう React のpropsという機能を使ってテキストの値を可変にします。propsを使うと呼び出し元から値を受け取ることができます。
this.propsidtextを持ったオブジェクトを受け取るので、{text}textの値だけ取り出しています。
<src/components/Todo.js>
import React from 'react';

class Todo extends React.Component {
  render() {
    const { text } = this.props 
    return (
      <div>
        <label>
          <input type="checkbox" />
          {text}
        </label>
        <button>編集</button>
        <button>削除</button>
      </div>
    )
  }
}

export default Todo;

App.jsを修正します。
タスク表示部分を Todo コンポーネントに置き換えます。
この時点でも画面に表示される情報は今までと同じ状態です。
<src/components/App.js>
import React from 'react';

import Form from  './Form';
// ----- 追加 -----
import Todo from './Todo';

class App extends React.Component {
  render() {
    return (
      <div>
        <Form />
        <label>
          <input type="checkbox" />
          全て完了にする
        </label>

        <select>
          <option>全て</option>
          <option>未完了</option>
          <option>完了済み</option>
        </select>

        <ul>
          // ----- Todoに修正 -----
          <li>
            <Todo id={0} text="朝ごはんを食べる" />
          </li>
          <li>
            <Todo id={0} text="筋トレをする" />
          </li>
          <li>
            <Todo id={0} text="勉強をする" />
          </li>
        </ul>
        <button>完了済みを全て削除</button>
      </div>
    )
  };
}

export default App;


*タスク追加機能の実装

入力したタスクを動的に追加できるようForm.jsを修正します。
追加ボタンが押されたときに動作するhandleSubmit()を新規作成し、これを form タグのonSubmitイベントで呼び出すようにします。
form をsubmitするとページ遷移してしまうので、その挙動をとめるためにhandleSubmit()のなかでpreventDefault()を使います。
タスクの追加後は入力フォームが空の状態にしたいので、this.setState({ input: '' })で初期化しています。
<src/components/Form.js>
import React from 'react';

class Form extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      input: ''
    };
  }
  render() {
    return (
      // ----- onSubmitを追加 -----
      <form onSubmit={this.handleSubmit}>
        <input type="text" value={this.state.input} onChange={this.handleChange} />
        <button>追加</button>
      </form>
    );
  }
  handleChange = e => {
    this.setState({ input: e.currentTarget.value })
  };
  // ----- 追加 -----
  handleSubmit = e => {
    e.preventDefault();
    this.setState({ input: '' })
  }
}

export default Form;

追加したタスクを管理できるようにするため、App.jsstateを追加します。
constructor()を追加しthis.stateでタスクのリストを定義します。
次に Todo コンポーネントを呼び出す ul タグの中でstate.todosを使うように修正します。
画面に表示される情報は今までと同じ状態です。
<src/components/App.js>
import React from 'react';

import Form from  './Form';
import Todo from './Todo';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      todos: [
        {
          id: 0,
          text: "朝ごはんを食べる"    
        },
        {
          id: 1,
          text: "筋トレをする"    
        },
        {
          id: 2,
          text: "勉強をする"    
        }
      ]
    };
  }
  render() {
    return (
      <div>
        <Form />
        <label>
          <input type="checkbox" />
          全て完了にする
        </label>

        <select>
          <option>全て</option>
          <option>未完了</option>
          <option>完了済み</option>
        </select>

        <ul>
          {this.state.todos.map(({ id, text }) => (
            <li key={id}>
              <Todo text={text}/>
            </li>
          ))}
        </ul>
        <button>完了済みを全て削除</button>
      </div>
    )
  };
}

export default App;

タスクをstateに置き換えることができたので、次は入力された値が実際に表示されるようApp.jsを修正します。
サンプルとして作成していたtodosのリストの値を削除します。
<src/components/App.js>
...
class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      // ----- データを削除 -----
      todos: []
    };
  }
...

さらにApp.jsに、追加ボタンが押されたときの処理handleSubmit()を実装します。
追加タスクの ID を連番にしたいのでcurrentIdを定義し、タスク追加後に +1 していきます。
...this.state.todosでは今まで入力されてたタスクの配列を展開しています。この配列の末尾に新しいタスクを追加し、新しい配列としてstateに登録します。

propsを使いonSubmitとして Form コンポーネントにhandleSubmit()を渡すようにします。(これをForm コンポーネントから呼び出すようにします)
<src/components/App.js>
import React from 'react';

import Form from  './Form';
import Todo from './Todo';

// ----- 追加 -----
let currentId = 0;

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      todos: []
    };
  }
  render() {
    return (
      <div>
        // ----- onSubmitを追加 -----
        <Form onSubmit={this.handleSubmit} />
        <label>
          <input type="checkbox" />
          全て完了にする
        </label>

        <select>
          <option>全て</option>
          <option>未完了</option>
          <option>完了済み</option>
        </select>

        <ul>
          {this.state.todos.map(({ id, text }) => (
            <li key={id}>
              <Todo text={text}/>
            </li>
          ))}
        </ul>
        <button>完了済みを全て削除</button>
      </div>
    );
  }
  // ----- 追加 -----
  handleSubmit = text => {
    const newTodo = {
      id: currentId,
      text: text,
    }
    const newTodos = [...this.state.todos, newTodo]
    this.setState({ todos: newTodos })
    currentId++;
  };
}

export default App;

Form.jsを修正します。
propsを受け取り、App.jshandleSubmit()を呼び出すようにします。
<src/components/Form.js>
import React from 'react';

class Form extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      input: ''
    };
  }
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input type="text" value={this.state.input} onChange={this.handleChange} />
        <button>追加</button>
      </form>
    );
  }
  handleChange = e => {
    this.setState({ input: e.currentTarget.value })
  };

  handleSubmit = e => {
    e.preventDefault();
    // ----- onSubmit()の呼び出しを追加 -----
    this.props.onSubmit(this.state.input);
    this.setState({ input: '' })
  }
}

export default Form;

ここまで実装すると、入力したタスクが画面に表示されるようになります。


*完了チェックボックスの実装

チェックボックスにチェックを入れたタスクは、完了したタスクとして管理できるようにしたいため、stateで管理する値にチェックの状態も追加します。

App.jshandleSubmit()で新しいタスクをstateに登録しているので、ここにcompletedの値を追加します。(初期値はfalse
次に、Todo コンポーネントにcompletedも渡すようpropsに追加します。さらにチェックボックスに変更があったときに動作するhandleChangeCompleted()を作成し、Todo コンポーネントのonChangeイベントとして設定します。
<src/components/App.js>
import React from 'react';

import Form from  './Form';
import Todo from './Todo';

let currentId = 0;

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      todos: []
    };
  }
  render() {
    return (
      <div>
        <Form onSubmit={this.handleSubmit} />
        <label>
          <input type="checkbox" />
          全て完了にする
        </label>

        <select>
          <option>全て</option>
          <option>未完了</option>
          <option>完了済み</option>
        </select>

        <ul>
          {this.state.todos.map(({ id, text, completed }) => (
            <li key={id}>
              // ----- completed,onChangeを追加 -----
              <Todo id={id} text={text} completed={completed} onChange={this.handleChangeCompleted} />
            </li>
          ))}
        </ul>
        <button>完了済みを全て削除</button>
      </div>
    );
  }
  handleSubmit = text => {
    const newTodo = {
      id: currentId,
      text: text,
      completed: false
    }
    const newTodos = [...this.state.todos, newTodo]
    this.setState({ todos: newTodos })
    currentId++;
  }
  // ----- 追加 -----
  handleChangeCompleted = (id, completed) => {
    const newTodos = this.state.todos.map(todo => {
      if (todo.id === id) {
        return {
          ...todo,
          completed
        }
      }
      return todo
    })
    this.setState({ todos: newTodos})
  }
}

export default App;

Todo.jsを修正します。
handleChangeCompleted()を追加してチェックボックスに変更があったときに動くようonChangeイベントとして設定します。
propscompletedには現在の値が入っているので、これを逆の値に修正してApp.jshandleChangeCompleted()を実行します。
<src/components/Todo.js>
import React from 'react';

class Todo extends React.Component {
  render() {
    const { text, completed } = this.props 
    return (
      <div>
        <label>
          // ----- checked,onChangeを追加 -----
          <input type="checkbox" checked={completed} onChange={this.handleChangeCompleted} />
          {text}
        </label>
        <button>編集</button>
        <button>削除</button>
      </div>
    )
  }
  // ----- 追加 -----
  handleChangeCompleted = () => {
    const { onChange, id, completed } = this.props;
    onChange(id, !completed);
  }
}

export default Todo;


*一括チェックボックスの実装

一括チェックボックスがチェックされると全タスクにチェックが入り、一括チェックボックスのチェックがはずれると全タスクのチェックがはずれる機能を実装します。
また、全タスクにチェックを入れたときに、一括チェックボックスにもチェックが入るようにします。(表示も変更します)
components/CheckAll.jsを新規作成します。
propsとして一括チェックボックスの値allCompletedを受け取るようにします。javaScript の仕様で、input タグでcheckedを使う場合は必ずonChangeを指定する必要があります。
<src/components/CheckAll.js>
import React from 'react';

class CheckAll extends React.Component {
  render() {
    const { allCompleted } = this.props
    return (
      <label>
        <input type="checkbox" checked={allCompleted} onChange={this.handleChange} />
        全て{allCompleted ? '未完了' : '完了'}にする
      </label>
    );
  }

  handleChange = () => {
    const { onChange, allCompleted } = this.props;
    onChange(!allCompleted)
  }
}

export default CheckAll;

App.jsを修正します。
handleChangeAllCompleted()を作成し、CheckAll コンポーネントから呼び出すようpropsonChangeとして渡します。
everyというのは配列が条件をすべて満たす場合にtrueを返す javaScript の機能です。初期表示時(todosが空)にこの条件がtrueになってチェックが付いてしまうため、todos.length > 0で条件を追加しています。
<src/components/App.js>
import React from 'react';

import Form from  './Form';
import Todo from './Todo';
// ----- 追加 -----
import CheckAll from './CheckAll';

let currentId = 0;

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      todos: []
    };
  }
  render() {
    // ----- stateを取得 -----
    const { todos } = this.state;
    return (
      <div>
        <Form onSubmit={this.handleSubmit} />
        // ----- CheckAllに置き換え -----
        <CheckAll 
          allCompleted={todos.length > 0 && todos.every(({ completed }) => completed)}
          onChange={this.handleChangeAllCompleted} />
        <select>
          <option>全て</option>
          <option>未完了</option>
          <option>完了済み</option>
        </select>

        <ul>
          {this.state.todos.map(({ id, text, completed }) => (
            <li key={id}>
              <Todo id={id} text={text} completed={completed} onChange={this.handleChangeCompleted} />
            </li>
          ))}
        </ul>
        <button>完了済みを全て削除</button>
      </div>
    );
  }
  handleSubmit = text => {
    const newTodo = {
      id: currentId,
      text: text,
      completed: false
    }
    const newTodos = [...this.state.todos, newTodo]
    this.setState({ todos: newTodos })
    currentId++;
  }
  // ----- 追加 -----
  handleChangeAllCompleted = completed => {
    const newTodos = this.state.todos.map(todo => ({
      ...todo,
      completed
    }));
    this.setState({ todos: newTodos });
  }
  handleChangeCompleted = (id, completed) => {
    const newTodos = this.state.todos.map(todo => {
      if (todo.id === id) {
        return {
          ...todo,
          completed
        }
      }
      return todo
    })
    this.setState({ todos: newTodos})
  }
}

export default App;





















*完了タスク一括削除ボタンの実装

一括削除ボタンを押すと、完了チェックボックスにチェックされていたタスクが全て削除されるようにします。単純な処理なため、コンポーネント化はしないでApp.jsに処理を追加しました。
App.jshandleClickDeleteCompleted()を追加し、filter() で完了していないタスクのみを取得して新しい配列としてstateに保存します。このメソッドをボタンが押されたときに呼び出すようonClickイベントに登録します。
<src/components/App.js>
...
// ----- onClickを追加 -----
<button onClick={this.handleClickDeleteCompleted}>完了済みを全て削除</button>

...

  // ----- 追加 -----
  handleClickDeleteCompleted = () => {
    const newTodos = this.state.todos.filter(({ completed }) => !completed)
    this.setState({ todos: newTodos })
  }
}



*フィルター機能の実装

タスクをセレクトボックスの「全て、未完了、完了済み」で絞り込んで表示できるようにします。
components/Filter.jsを新規作成します。
セレクトボックスの値をそれぞれvalueで定義しておく必要があります。propsでセレクトボックスの値を受け取り、変更があった場合にApp.jsからonChangeとして渡されたメソッドを呼び出します。
<src/components/Filter.js>
import React from "react";

class Fileter extends React.Component {
  render() {
    const { filter } = this.props;
    return (
      <select value={filter} onChange={this.handleChange}>
        <option value="all">全て</option>
        <option value="uncompleted">未完了</option>
        <option value="completed">完了済み</option>
      </select>
    );
  }

  handleChange = e => {
    this.props.onChange(e.currentTarget.value);
  };
}

export default Fileter;

App.jsを修正します。
セレクトボックスの部分を Filter コンポーネントに置き換えます。
また、セレクトボックスの値を管理できるようにしたいため、statefilterの値を追加します。(初期値はall
次にhandleChangeFilter()を追加し、statefilterを更新するようにします。このメソッドを Filter コンポーネントにpropsで渡して呼び出してもらうようにします。
filter()した結果となるfilteredTodosを新しく作成し、これを読み込んで画面に表示させます。
<src/components/App.js>
import React from "react";

import Form from "./Form";
import Todo from "./Todo";
import CheckAll from "./CheckAll";
// ----- 追加 -----
import Filter from "./Filter";

let currentId = 0;

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      // ----- 追加 -----
      filter: "all",
      todos: []
    };
  }
  render() {
    // ---- フィルターする値を取得 ----
    const { todos, filter } = this.state;
    // ---- フィルターしたリストを取得 ----
    const filteredTodos = todos.filter(({ completed }) => {
      switch (filter) {
        case "all":
          return true;
        case "uncompleted":
          return !completed;
        case "completed":
          return completed;
        default:
          return true;
      }
    });
    return (
      <div>
        <Form onSubmit={this.handleSubmit} />
        <CheckAll
          allCompleted={
            todos.length > 0 && todos.every(({ completed }) => completed)
          }
          onChange={this.handleChangeAllCompleted}
        />
        // ----- 追加 -----
        <Filter filter={filter} onChange={this.handleChangeFilter} />
        <ul>
          // ----- フィルター後のリストに修正 -----
          {filteredTodos.map(({ id, text, completed }) => (
            <li key={id}>
              <Todo
                id={id}
                text={text}
                completed={completed}
                onChange={this.handleChangeCompleted}
              />
            </li>
          ))}
        </ul>
        <button onClick={this.handleClickDeleteCompleted}>
          完了済みを全て削除
        </button>
      </div>
    );
  }
  handleSubmit = text => {
    const newTodo = {
      id: currentId,
      text: text,
      completed: false
    };
    const newTodos = [...this.state.todos, newTodo];
    this.setState({ todos: newTodos });
    currentId++;
  };

  handleChangeAllCompleted = completed => {
    const newTodos = this.state.todos.map(todo => ({
      ...todo,
      completed
    }));
    this.setState({ todos: newTodos });
  };
  // ---- 追加 ----
  handleChangeFilter = filter => {
    this.setState({ filter });
  };
  handleChangeCompleted = (id, completed) => {
    const newTodos = this.state.todos.map(todo => {
      if (todo.id === id) {
        return {
          ...todo,
          completed
        };
      }
      return todo;
    });
    this.setState({ todos: newTodos });
  };
  handleClickDeleteCompleted = () => {
    const newTodos = this.state.todos.filter(({ completed }) => !completed);
    this.setState({ todos: newTodos });
  };
}

export default App;


*削除機能を実装

タスクにそれぞれ付いている削除ボタンを押すと、そのタスクが削除されるようにします。
Todo.jsを修正します。
handleClickDelete()を作成し、削除ボタンが押されたときにonClickイベントとして呼び出すように設定します。
どのタスクを削除するのか知る必要があるので、propsidを受け取ります。propsとして受け取ったonDeleteのメソッドを呼び出すようにします。
<src/components/Todo.js>
import React from "react";

class Todo extends React.Component {
  render() {
    const { text, completed } = this.props;
    return (
      <div>
        <label>
          <input
            type="checkbox"
            checked={completed}
            onChange={this.handleChangeCompleted}
          />
          {text}
        </label>
        <button>編集</button>
        // ----- onClickを追加 -----
        <button onClick={this.handleClickDelete}>削除</button>
      </div>
    );
  }
  handleChangeCompleted = () => {
    const { onChange, id, completed } = this.props;
    onChange(id, !completed);
  };
  // ----- 追加 -----
  handleClickDelete = () => {
    const { onDelete, id } = this.props;
    onDelete(id);
  };
}

export default Todo;

App.jsを修正します。
handleClickDelete()を作成し、削除するタスク以外で新しい配列を作成してstateに登録します。
さらに Todo コンポーネントから呼び出してもらえるようpropsonDeleteとしてこのメソッドを渡します。
<src/components/App.js>
        <ul>
          {filteredTodos.map(({ id, text, completed }) => (
            <li key={id}>
              <Todo
                id={id}
                text={text}
                completed={completed}
                onChange={this.handleChangeCompleted}
                // ----- onDeleteを追加 -----
                onDelete={this.handleClickDelete}
              />
            </li>
          ))}
        </ul>

...

  // ----- 追加 -----
  handleClickDelete = id => {
    const newTodos = this.state.todos.filter(todo => todo.id !== id);
    this.setState({ todos: newTodos });
  };


*編集機能の実装

タスクにそれぞれ付いている編集ボタンを押すと、そのタスク名を更新できるようにします。
編集時は別コンポーネントにしたいため、components/EditTodo.jsを新規作成します。
編集中にキャンセルボタンを押すと元に戻り、更新ボタンを押すと新しく入力されたタスク名に更新されます。
<src/components/EditTodo.js>
import React from "react";

class EditTodo extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      text: props.text
    };
  }
  render() {
    return (
      <div>
        <input
          type="text"
          value={this.state.text}
          onChange={this.handleChange}
        />
        <button onClick={this.handleClickCancel}>キャンセル</button>
        <button onClick={this.handleSubmit}>更新</button>
      </div>
    );
  }

  handleChange = e => {
    this.setState({ text: e.currentTarget.value });
  };
  handleClickCancel = () => {
    const { onCancel, id } = this.props;
    onCancel(id, "editing", false);
  };
  handleSubmit = () => {
    const { onSubmit, id } = this.props;
    if (!this.props.text) return;
    onSubmit(id, this.state.text);
  };
}

export default EditTodo;

App.jsを修正します。
タスクの登録時に動作するhandleSubmit()で、stateとして保存するtodosの値にeditingを追加します。(初期値はfalse)このeditingtrueのときに EditTodo コンポーネントが呼び出されるようにします。
handleUpdateTodoText()を作成して EditTodo コンポーネントから呼び出されるようpropsで渡します。編集後はeditingの値は必ずfalseになります。

Todo コンポーネントのonChangeイベントで使っていたhandleChangeCompleted()を EditTodo コンポーネントでも使えるようにしたいため、メソッド名をhandleChangeTodoAttribute()に修正しkeyvalueで更新できるように引数と処理を修正します。
<src/components/App.js>
import React from "react";

import Form from "./Form";
import Todo from "./Todo";
import CheckAll from "./CheckAll";
import Filter from "./Filter";
// ----- 追加 -----
import EditTodo from "./EditTodo";

let currentId = 0;

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      filter: "all",
      todos: []
    };
  }
  render() {
    const { todos, filter } = this.state;
    const filteredTodos = todos.filter(({ completed }) => {
      switch (filter) {
        case "all":
          return true;
        case "uncompleted":
          return !completed;
        case "completed":
          return completed;
        default:
          return true;
      }
    });
    return (
      <div>
        <Form onSubmit={this.handleSubmit} />
        <CheckAll
          allCompleted={
            todos.length > 0 && todos.every(({ completed }) => completed)
          }
          onChange={this.handleChangeAllCompleted}
        />
        <Filter filter={filter} onChange={this.handleChangeFilter} />
        <ul>
          {filteredTodos.map(({ id, text, completed, editing }) => (
            <li key={id}>
              // ----- 分岐処理を追加 -----
              {editing ? (
                <EditTodo
                  id={id}
                  text={text}
                  onCancel={this.handleChangeTodoAttribute}
                  onSubmit={this.handleUpdateTodoText}
                />
               ) : (
                <Todo
                  id={id}
                  text={text}
                  completed={completed}
                  onChange={this.handleChangeTodoAttribute}
                  onDelete={this.handleClickDelete}
                />
              )}
            </li>
          ))}
        </ul>
        <button onClick={this.handleClickDeleteCompleted}>
          完了済みを全て削除
        </button>
      </div>
    );
  }
  handleSubmit = text => {
    // ----- editingを追加 -----
    const newTodo = {
      id: currentId,
      text: text,
      completed: false,
      editing: false
    };
    const newTodos = [...this.state.todos, newTodo];
    this.setState({ todos: newTodos });
    currentId++;
  };

  handleChangeAllCompleted = completed => {
    const newTodos = this.state.todos.map(todo => ({
      ...todo,
      completed
    }));
    this.setState({ todos: newTodos });
  };
  handleChangeFilter = filter => {
    this.setState({ filter });
  };
  // ----- key,valueで更新できるよう修正 -----
  handleChangeTodoAttribute = (id, key, value) => {
    const newTodos = this.state.todos.map(todo => {
      if (todo.id === id) {
        return {
          ...todo,
          [key]: value
        };
      }
      return todo;
    });
    this.setState({ todos: newTodos });
  };
  handleUpdateTodoText = (id, text) => {
    const newTodo = this.state.todos.map(todo => {
      if (todo.id === id) {
        return {
          ...todo,
          text,
          editing: false
        };
      }
      return todo;
    });
    this.setState({ todos: newTodo });
  };

  handleClickDelete = id => {
    const newTodos = this.state.todos.filter(todo => todo.id !== id);
    this.setState({ todos: newTodos });
  };
  handleClickDeleteCompleted = () => {
    const newTodos = this.state.todos.filter(({ completed }) => !completed);
    this.setState({ todos: newTodos });
  };
}

export default App;

Todo.jsを修正します。
handleClickEdit()を作成し、編集ボタン押下時に動作するようonClickイベントに設定します。
App.jsでの修正に伴い、handleChangeCompleted()onChangeの引数がid, key, valueになるよう修正します。
<src/components/Todo.js>
import React from "react";

class Todo extends React.Component {
  render() {
    const { text, completed } = this.props;
    return (
      <div>
        <label>
          <input
            type="checkbox"
            checked={completed}
            onChange={this.handleChangeCompleted}
          />
          {text}
        </label>
        <button onClick={this.handleClickEdit}>編集</button>
        <button onClick={this.handleClickDelete}>削除</button>
      </div>
    );
  }
  handleChangeCompleted = () => {
    const { onChange, id, completed } = this.props;
    onChange(id, "completed", !completed);
  };
  handleClickEdit = () => {
    const { onChange, id, editing } = this.props;
    onChange(id, "editing", !editing);
  };
  handleClickDelete = () => {
    const { onDelete, id } = this.props;
    onDelete(id);
  };
}

export default Todo;

補足ですが、何も入力していない状態でタスクを追加できないようにForm.jshandleSubmit()に処理を追加します。
<src/components/Form.js>
import React from "react";

class Form extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      input: ""
    };
  }
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input
          type="text"
          value={this.state.input}
          onChange={this.handleChange}
        />
        <button>追加</button>
      </form>
    );
  }
  handleChange = e => {
    this.setState({ input: e.currentTarget.value });
  };
  handleSubmit = e => {
    e.preventDefault();
    // ----- if文を追加 -----
    if (!this.state.input) return;
    this.props.onSubmit(this.state.input);
    this.setState({ input: "" });
  };
}

export default Form;

完成した画面です。





























*所感

動画を参考にさせていただいたのですが、コンポーネントの分割方法など順を追って進めてくださり、React について丁寧に解説もしてくださったので大変わかりやすかったです。
業務では Vue.js を使っており Vue のほうが簡単だと思っていましたが、コンポーネントに分割するという面では React も Vue と同様で、DOM要素を直接書ける React も使いやすいと思ってきました。
今回は Redux を使わなかったので React に集中して学べたこともあり、基礎的な部分についてはかなり理解が深まりました。Redux や スタイルの導入などについても理解を深めていきたいと思います。

Previous
Next Post »

人気の投稿