React.js で作る"Hello React" からのToDoアプリを作成する~その2: ToDoアプリ完成~

前回の記事で取りあえずは"Hello World!!"が表示された。これをベースにToDo アプリを作っていく。hirahiro56.hatenablog.com

下記のようなアプリイメージのものを開発していく。

f:id:hirahiro56:20150516135323j:plain


【Step.1】index.htmlの作成
まずは、index.htmlを準備する。これはHello World の時と何にも変わらない。

<html>
  <head>
  </head>
  <body>
    <div id="app-container"></div>
    <script src="dist/app.js"></script>
  </body>
</html>

【Step.2】TodoListコンポーネントの作成
アプリのルートとなるTodoListコンポーネントを作成する。このコンポーネントは全てのコンポーネントの親玉となるもので、このアプリにおいて状態を監視するべきデータである「残タスク一覧」(実体はオブジェクトのコレクション)を管理する。React.js では状態を管理する必要のあるデータ、つまり、更新されると画面を再描画する必要がるものは全て親玉(ルート)のコンポーネントで管理するように設計する。このような管理するべきデータをstete と呼ぶ。
コードは下記のようになり、app.js として保存する。

var React = require('react');
var Todo = require('./todo');
var InputApp = require('./inputapp');

var generateId = (
	function(){
		var id = 10;
		return function(){
			return (id++);
		}
	}
)();

var TodoList = React.createClass({
 //todos(ToDoリスト)を初期化
  getInitialState() {
    return {
      todos: [
        {id:1, text:"Sample1"},
        {id:2, text:"Sample2"},
        {id:3, text:"Sample3"}
      ]
    };
  },
  //引数のidに一致するデータをtodosから削除
  deleteTodo(id) {
    this.setState({
      todos: this.state.todos.filter((todo) => {
        return todo.id !== id;
      })
    });
  },
  //引数のToDoをToDoリスト(todos)にアペンドする。
  updateToDoList:function(todo){
    var tmptodos = this.state.todos.concat(todo);
    this.setState({todos: tmptodos});
  },
  render() {
    //ToDoリストのデータをMarkdown形式で書き換える。
    var todos = this.state.todos.map((todo) => {
      return <li key={todo.id}><Todo onDelete={this.deleteTodo} todo={todo} /></li>;
    });
    return (
      <div>
        <InputApp onSave={this.updateToDoList} id={generateId()} />
        <ul>{todos}</ul>
      </div>
    );
  }
});

React.render(
  <TodoList />, document.getElementById('app-container')
);

上記コードから、このコンポーネントは、Todo, およびInputAppという子コンポーネントを包括していることが分かると思う。
次のステップで、これらのコンポーネントを実装していくことになる。

【Step.3】InputAppコンポーネントの実装
InputAppコンポーネントの役割は下記2つとなる。
1.フォームの表示
2.フォームの内容(INPUT)を todos に追加する(ToDoリストに追加する)

ソースファイルをinputapp.js とし、下記の通り実装する。

var React = require('react');

var InputApp = React.createClass({
  _onKeyDown:function(e){
    if(e.keyCode === 13){
      var todo = {
        id: '',
        text: ''
      };
      todo.text = e.target.value;
      todo.id = this.props.id;
      this.props.onSave(todo);
      //this.props.onSave(e.target.value);
      e.target.value="";
    }
  },
  render: function(){
      return (
        <div>
          <h2>Input your ToDo</h2>
          <input type="text" onKeyDown={this._onKeyDown} />
        </div>
      );
  }
});

module.exports = InputApp;

【Step.4】ToDoコンポーネントの実装
ToDoコンポーネントの役割としては、下記2つとなる。
1.登録されたToDoの内容を表示する
2.delete ボタン押下時にToDoリスト(todos)から要素を削除する

todo.js として下記の通り実装するが、
削除処理について、実体は親コンポーネント(ToDoList)のdeleteTodoメソッドとなる。つまり、ToDoコンポーネントではToDoリスト(todos)の内容を更新するようなことはせず、親コンポーネントに処理を委譲する(呼び出し & 引数を与える)だけである。React.jsではアプリケーションの状態、所謂state(ToDoリストのtodos[])の管理は親玉(ルート)のコンポーネントに一括で請け負ってもらい、子コンポーネントはstateを持たないように設計することが定石となる。

var React = require('react');

var Todo = React.createClass({
  propTypes: {
    todo: React.PropTypes.shape({
      id: React.PropTypes.number.isRequired,
      text: React.PropTypes.string.isRequired
    }),
    // 削除するための処理をI/Fとして定義
    onDelete: React.PropTypes.func.isRequired
  },
  // 親に処理を委譲する
  _onDelete() {
    this.props.onDelete(this.props.todo.id);
  },
  render() {
    return (
      <div>
        <span>{this.props.todo.text}</span>
        <button onClick={this._onDelete}>delete</button>
      </div>
    );
  }
});

module.exports = Todo;

【Step.5】ビルド
最後にbrowserify/reactifyでビルドを実行するが、本ソースはES6, 7のシンタックスを利用しているため、reactifyのharmony オプションをつけてビルドする。

browserify -t [reactify --harmony ]  src/app.js -o dist/app.js
npm start

上記コマンド実行したら下記URLにアクセスして、ToDoアプリの挙動を確認できる。

http://localhost:8080/