はじめてのReact #20「ToDoアプリを作る」 前編

前回準備した環境を使って、早速Webアプリを作ってみたいと思います。こういうときはToDoアプリを作るのが伝統となっていますので、それに習いますw

なお、本来であればテストコードを書きながら進めるべきだとは思いますが、今回は割愛しております。

準備編

プロジェクトの作成

rtodoという名前でプロジェクトを新規作成しました。rはReactで作るぞという意思表示で特に意味はありませんw 好きな名称をつけてください。

$ create-react-app rtodo

Gitリポジトリの準備

GitHubにもrtodoという名前でリポジトリを準備しました。まずはこいつにgit pushします。 https://github.com/katsube/rtodo

$ git remote add origin git@github.com:katsube/rtodo.git
$ git push -u origin master

Componentを作ってみる

新しいComponentを作る

まずは腕鳴らしに簡単なComponentを用意してみます。src/header.jsというファイルを作成し、その中に以下のコードを記述します。render()でタグを描画しているだけですね。h1の中はprops.childrenでもらった文字列をそのまま出しています。

import React, { Component } from 'react';

class Header extends Component {
  render() {
    return(
      <header>
        <h1>{this.props.children}</h1>
      </header>
    );
  }
}

export default Header;

最後のexport文を忘れないように気をつけてください。これを記述することで他のファイルからこのComponentを利用することができるようになります。

子供の要素にアクセスする方法(this.props.children)については過去の記事を参照してください。 blog.katsubemakito.net

App.jsから呼び出す

src/App.jsを開き以下のコードに書き換えます。

import React, { Component } from 'react';
import Header from './header';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <Header>
          React ToDo
        </Header>
      </div>
    );
  }
}

export default App;

先ほど作成したHeaderComponentを、2行目のimport文で呼び出しています。これ以降はHeaderという名前でHeaderComponentを利用することができるようになります。最終的にAppComponentのrender()で先ほど作成したHeaderComponentを利用しているのがわかります。

実行してみる

前回説明した通り、まずは無事に動くかプレビューしてみます。

$ npm start

最終的にブラウザが立ち上がり、以下のような表示になれば成功です。

引き続き開発を続けるため、Terminalはこのままの状態にしておきます。npm startでプレビュー用のサーバを起動するのは非常に時間がかかるため動かしっぱなしの方がストレスがたまりません。

コミット

Terminalのウィンドウかタブを新たに作成し、commitとpushしておきましょう。

$ git add .
$ git commit -m 'add HeaderComponent'
$ git push

あとはひたすらこの流れを繰り返します。 本来はテストコードも同時に書いてチェックした方が良いのですが、今回は割愛します。

ToDoの一覧を表示する

ちょっとずつ作って行きましょう。まずは予め定義したToDo一覧を表示してみます。

App.js

App.jsの変更点は3行目のimport文で新たにToDoComponentを読み込み、render()内で利用しています。

import React, { Component } from 'react';
import Header from './header';
import ToDo from './todo';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <Header>
          React ToDo
        </Header>
        <ToDo />
      </div>
    );
  }
}

export default App;

todo.js

新たにsrc/todo.jsを作成し、state内のToDoリストを単純に描画するだけのrender()を書いています。

import React, { Component } from 'react';
import './todo.css';

class ToDo extends Component {
  constructor(props){
    super(props);
    this.state = {
      todo: [
        {id:1, name:"食材を買いに行く"},
        {id:2, name:"チャーハンを調理する"},
        {id:3, name:"チャーハンを盛り付ける"}
      ]
    };
  }
  render() {
    let list = this.state.todo.map( item =>
                 <li key={item.id}>{item.name}</li>
               );

    return(
      <ul class="todolist">
        {list}
      </ul>
    );
  }
}

export default ToDo;

CSSも編集してみる

まずはsrc/App.cssにあった利用しない物を削除し、以下のCSSを記述します。

.App {
  text-align: center;
  width: 300px;
}

新規にtodo.cssを作成し以下のCSSを記述しています。このようにComponent毎にCSSファイルを用意すると管理がしやすいですね。

.todolist{
  text-align: left;
}

実行結果

ファイルに保存した時点で自動的にブラウザの方はリロードされているはずです。 以下のような結果になれば成功です。

新しいToDoを追加する

新しいToDoを登録する機能を追加してみます。ToDo一覧の上に入力フォームを設置し、ボタンをクリックすると登録されます。

todo.js

stateには2つのプロパティを追加しています。

  • 入力されたテキストボックスの値を保存 (newtodo)
  • IDの最大値を管理 (max_id)

またそれぞれ入力された、クリックされたといったイベント処理が追加されています。

import React, { Component } from 'react';
import './todo.css';

class ToDo extends Component {
  constructor(props){
    super(props);
    this.state = {
      newtodo: "",
      todo: [
        {id:1, name:"食材を買いに行く"},
        {id:2, name:"チャーハンを調理する"},
        {id:3, name:"チャーハンを盛り付ける"}
      ],
      max_id: 3
    };

    this.handleChange = this.handleChange.bind(this);
    this.handleClick  = this.handleClick.bind(this);
  }

  handleChange(e){
    this.setState({
      newtodo: e.target.value
    });
  }

  handleClick(){
    let id   = this.state.max_id;
    let todo = this.state.todo;
    todo.push({id: id+1, name:this.state.newtodo});

    this.setState({
        newtodo: "",
        todo: todo,
        max_id: id + 1
    });
  }

  render() {
    let list = this.state.todo.map( item =>
                 <li key={item.id}>{item.name}</li>
               );

    return(
      <div>
        <form>
          <input type="text" value={this.state.newtodo} onChange={this.handleChange}/>
          <input type="button" value="追加" onClick={this.handleClick} />
        </form>
        <ul class="todolist">
          {list}
        </ul>
      </div>
    );
  }
}

export default ToDo;

テキストボックスの操作関連は過去の記事を参照してください。 blog.katsubemakito.net

ファイルに保存したらブラウザで動作確認をしてみましょう。

入力フォームを別のComponentにわける

このままでも動いているので問題ないと言えば無いのですが、メンテナンス性を上げるためにさらに細かくComponentを分けてみます。

todo.js

入力フォーム部分をToDoCreateComponentに切り出しました。 テキストボックスの入力を管理していた以下の要素がtodo.jsからなくなっています。

  • stateのnewtodo
  • handleChange()メソッド

handleClick()の引数は、これまではStateを参照していましたが、ToDoCreateから渡されることになります。

import React, { Component } from 'react';
import ToDoCreate from './todocreate';
import './todo.css';

class ToDo extends Component {
  constructor(props){
    super(props);
    this.state = {
      todo: [
        {id:1, name:"食材を買いに行く"},
        {id:2, name:"チャーハンを調理する"},
        {id:3, name:"チャーハンを盛り付ける"}
      ],
      max_id: 3
    };

    this.handleClick  = this.handleClick.bind(this);
  }

  handleClick(value){
    let id   = this.state.max_id;
    let todo = this.state.todo;
    todo.push({id: id+1, name:value});

    this.setState({
        todo: todo,
        max_id: id + 1
    });
  }

  render() {
    let list = this.state.todo.map( item =>
                 <li key={item.id}>{item.name}</li>
               );

    return(
      <div>
        <ToDoCreate onClick={this.handleClick} />
        <ul class="todolist">
          {list}
        </ul>
      </div>
    );
  }
}

export default ToDo;

ToDoCreateには親Componentのメソッドが呼び出せるようにしています。

<ToDoCreate onClick={this.handleClick} />

このあたりのComponent間の連携については過去の記事を参照してください。 blog.katsubemakito.net

todocreate.js

入力している最中の処理はすべてToDoCreateで受け持ち、最終的に入力が終了した時点で親Componentを呼び出しています。

import React, { Component } from 'react';

class ToDoCreate extends Component {
  constructor(props){
    super(props);
    this.state = {
      newtodo: ""
    };

    this.handleChange = this.handleChange.bind(this);
    this.handleClick  = this.handleClick.bind(this);
  }

  handleChange(e){
    this.setState({
      newtodo: e.target.value
    });
  }

  handleClick(){
    this.props.onClick(this.state.newtodo);
    this.setState({newtodo:""});
  }

  render(){
    return(
      <form>
        <input type="text" value={this.state.newtodo} onChange={this.handleChange}/>
        <input type="button" value="追加" onClick={this.handleClick} />
      </form>
    );
  }
}

export default ToDoCreate;

各Componentで仕事が別れたため、職掌がハッキリしましたね。

ToDoリストも別Componentに分ける

せっかく登録フォームを分けたので、リストの方も別Componentにしてみます。またサンプルのToDoデータも今回から削除しました。

todo.js

リスト部分をToDoList Componentとして外に出しました。render()がスッキリしましたね! todo.csstodolist.cssgit mvでリネームしています。

import React, { Component } from 'react';
import ToDoCreate from './todocreate';
import ToDoList from './todolist';

class ToDo extends Component {
  constructor(props){
    super(props);
    this.state = {
      todo: [],
      max_id: 0
    };

    this.handleClick  = this.handleClick.bind(this);
  }

  handleClick(value){
    let id   = this.state.max_id;
    let todo = this.state.todo;
    todo.push({id: id+1, name:value});

    this.setState({
        todo: todo,
        max_id: id + 1
    });
  }

  render() {
    return(
      <div>
        <ToDoCreate onClick={this.handleClick} />
        <ToDoList data={this.state.todo} />
      </div>
    );
  }
}

export default ToDo;

todolist.js/css

データ自体は親ComponentであるToDoからpropsとしてもらいますので、ToDoListComponentでは単純に描画処理だけを行います。ToDoが存在しなければメッセージを、存在すればリストを表示します。

リストもToDoItem Componentとしてさらに外出ししています。

import React, { Component } from 'react';
import ToDoItem from './todoitem';
import './todolist.css';

class ToDoList extends Component {
  render(){
    let data = this.props.data;

    // ToDoがゼロ
    if( data.length === 0 ){
      return(
        <div className="todozero">Nothing ToDo</div>
      );
    }
    // ToDoが存在する
    else{
      return(
        <ul className="todolist">{
          data.map( i => <ToDoItem key={i.id} item={i} /> )
        }</ul>
      );

    }
  }
}

export default ToDoList;

以下はtodolist.cssとして保存します。

.todolist{
  text-align: left;
}
.todozero{
  text-align: center;
  margin: 20px;
}

todoitem.js

ToDoの要素を描画するComponentです。 なぜこんな細かくわけるのか、その恩恵は次回以降に判明すると思いますw

import React, { Component } from 'react';

class ToDoItem extends Component {
  render(){
    let id   = this.props.item.id;
    let name = this.props.item.name;
    return(
      <li key={id}>{name}</li>
    );
  }
}

export default ToDoItem;

実行結果

ここまでのコードはGitHubにあげてますので、必要に応じて参照ください。 github.com

続く

次回に続きます。 blog.katsubemakito.net

参考ページ

reactjs.org