Hello React World

Single Page Application を Angular で実装しようと思っていたんだけど、 Angular2 でコンポーネント指向にガラリと変わるみたいなので、 Angular の採用は見送って React を選択。 同じコンポーネント指向なら React でいいかな、と。

React 入門ということで TODO リストのサンプルを書いてみる。 毎度のこだわりとして、その場編集機能を実装している。

コピペするだけで試せるようにしたかったので、あえて事前にコンパイルする方法は取ってない。 おかげで画面が表示されるまで結構待たされるけど、手軽さ優先で我慢我慢。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>React Tutorial</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-dom.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
  </head>
  <body>
    <div id="content"></div>
    <script type="text/babel">
      // タスクを表示するコンポーネント
      var Task = React.createClass({
        handleEdit(e) {
          this.props.onTaskSave(this.props.task, {
            editing: true
          });
        },

        handleDelete(e) {
          this.props.onTaskDelete(this.props.task);
        },

        handleChangeCheckbox(e) {
          this.props.onTaskSave(this.props.task, {
            completed: !this.props.task.completed
          });
        },

        render() {
          // JSX の中で分岐する場合は即時関数を使う
          return (
            <div>
              <input type="checkbox"
                     checked={this.props.task.completed}
                     onChange={this.handleChangeCheckbox} />
              {(() => {
                if (this.props.task.completed) {
                  return <del>{this.props.task.name}</del>
                } else {
                  return <span>{this.props.task.name}</span>
                }
              }())}
              <a href="#" onClick={this.handleEdit}>[edit]</a>
              <a href="#" onClick={this.handleDelete}>[del]</a>
            </div>
          );
        }
      });

      // タスクをその場編集するためのコンポーネント
      var EditTaskForm = React.createClass({
        getInitialState() {
          return {
            name: ""
          };
        },

        handleCancel(e) {
          this.props.onTaskSave(this.props.task, {
            editing: false
          });
        },

        handleSave(e) {
          e.preventDefault();

          if (!this.state.name) {
            return;
          }

          this.props.onTaskSave(this.props.task, {
            name: this.state.name,
            editing: false
          });

          this.setState({ name: "" });
        },

        handleChangeText(e) {
          this.setState({ name: e.target.value });
        },

        render() {
          return (
            <div>
              <input type="text"
                     value={this.state.name || this.props.task.name}
                     onChange={this.handleChangeText}
                     ref="name"/>
              <button onClick={this.handleSave}>save</button>
              <button onClick={this.handleCancel}>cancel</button>
            </div>
          );
        }
      });

      // タスク一覧を表示するためのコンポーネント
      var TaskList = React.createClass({
        render() {
          var rows = this.props.tasks.map(t => {
            if (t.editing) {
              return (
                <EditTaskForm task={t}
                              onTaskSave={this.props.onTaskSave} />
              );
            } else {
              return (
                <Task task={t}
                      onTaskSave={this.props.onTaskSave}
                      onTaskDelete={this.props.onTaskDelete} />
              );
            }
          });

          return (
            <div>
              {rows}
            </div>
          );
        }
      });

      // タスク登録フォームを表示するためのコンポーネント
      var AddTaskForm = React.createClass({
        handleAdd(e) {
          e.preventDefault();

          var newTask = ReactDOM.findDOMNode(this.refs.name).value.trim();
          if (!newTask) {
            return;
          }

          this.props.onTaskAdd({ name: newTask });

          ReactDOM.findDOMNode(this.refs.name).value = "";
        },

        render() {
          return (
            <div>
              <input type="text" placeholder="newTask" ref="name"/>
              <button onClick={this.handleAdd}>add</button>
            </div>
          );
        }
      });

      // ルートコンポーネント
      // 状態をまとめて管理する
      var TaskApp = React.createClass({
        getInitialState() {
          return {
            tasks: [
              { name: "foo", completed: false, editing: false },
              { name: "bar", completed: false, editing: false },
              { name: "hoge", completed: false, editing: false },
              { name: "fuga", completed: false, editing: false }
            ]
          };
        },

        handleTaskAdd(newTask) {
          var newTasks = this.state.tasks.concat(newTask);
          this.setState({ tasks: newTasks });
        },

        handleTaskDelete(task) {
          var newTasks = this.state.tasks.filter(t => {
            return t !== task;
          });
          this.setState({ tasks: newTasks });
        },

        handleTaskSave(task, newValues) {
          var newTasks = this.state.tasks.map(t => {
            if (t === task) {
              return $.extend(task, newValues);
            } else {
              return t;
            }
          });
          this.setState({ tasks: newTasks });
        },

        render() {
          return (
            <div>
              <AddTaskForm onTaskAdd={this.handleTaskAdd} />
              <TaskList tasks={this.state.tasks}
                        onTaskSave={this.handleTaskSave}
                        onTaskDelete={this.handleTaskDelete} />
            </div>
          );
        }
      });
      

      ReactDOM.render(
        <TaskApp />,
        document.getElementById("content")
      );
    </script>
  </body>
</html>

ルートにだけ状態を持たせる作りに挑戦したけど、イベントの伝播が面倒だった。 この程度のサンプルで面倒に感じるんだから、もっと本格的なアプリになったら、 やっぱりフレームワークは必要だな。 Redux あたりを試してみようかな。