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

3回に渡ってお送りしたToDoアプリ開発も最終回。今回はAjaxを利用しデータをサーバに保存してみます。

Propsのデータ型をチェック

以前取り上げたPropsのデータ型のチェックを導入しておきます。 blog.katsubemakito.net

プロジェクトも運用段階に入りComponentがどこからどのように呼び出されるか、収集がつかなくなったとしても呼び出し方さえ守ってもらえれば何とかなったりするものです(それが良いかはさて置き)。また誤った利用方法をするとテスト段階でエラーを明示的に出してくれるのもありがたいものです。

src/header.js

import PropTypes from 'prop-types';

// <Header>
//   React ToDo
// </Header>
Header.propTypes = {
  children: PropTypes.string.isRequired
};

src/todocreate.js

import PropTypes from 'prop-types';

// <ToDoCreate onClick={this.handleClick} />
ToDoCreate.propTypes = {
  onClick: PropTypes.func.isRequired
};

src/todolist.js

import PropTypes from 'prop-types';

// <ToDoList data={this.state.todo} remove={this.handleRemove}/>
ToDoList.propTypes = {
  data: PropTypes.array.isRequired,
  remove: PropTypes.func.isRequired
};

src/todoitem.js

import PropTypes from 'prop-types';

// <ToDoItem key={i.id} item={i} remove={this.props.remove} />
ToDoItem.propTypes ={
  item: PropTypes.object.isRequired,
  remove: PropTypes.func.isRequired
};

データをサーバ側で管理する

APIサーバを用意

Reactから話がずれますが、学習用に保存や取り出しを行うAPIサーバをNode.jsで準備します。かなり簡易的な物なので本番投入はしないでください。しないとは思いますがw

まずは適当な名前のディレクトリを作成しカレントディレクトリを別の場所に移します。今のプロジェクトは全く別の場所に作成してください。その後簡単にHTTPサーバを作成できるexpressを入れます。

$ mkdir apiserve; cd apiserve
$ npm install express

詳細は説明しませんが、以下のスクリプトをexpressをインストールしたディレクトリに適当な名前で保存します。ここではserve.jsとしました。

APIサーバを起動します。

$ node serve.js
listening on *:3001

正常に動くか実験してみます。 通常3001番のポートで起動しますので、そこにPOSTメソッドで以下のようなJSONを/setに対して送信するとサーバ内部に保存してくれます。

$ curl -X POST -d 'data={"name":"foo"}' localhost:3001/set
{"status":"OK"}
$ curl -X POST -d 'data={"name":"bar"}' localhost:3001/set
{"status":"OK"}

取り出すときはGETメソッドで/getにリクエストします。これだけだと全件が返却されるのですが、/get/1などidを末尾につけると対象のレコードだけを取り出すことができます。気がついた方もいらっしゃると思いますが、各レコードのidはサーバ側で自動的に採番し付加されます。リクエスト時に指定した場合も上書きされるのでご注意ください。

$ curl localhost:3001/get
{"status":"OK","data":[{"id":1,"name":"foo"},{"id":2,"name":"bar"}]}

$ curl localhost:3001/get/1
{"status":"OK","data":[{"id":1,"name":"foo"}]}

レコードを削除する場合は/remove/1のようにリクエストを送ります。

$ curl localhost:3001/remove/1
{"status":"OK"}
$ curl localhost:3001/get
{"status":"OK","data":[{"id":2,"name":"bar"}]}

サーバを終了する場合はCtrl+cなどで終了させます。 なおデータはメモリ上にしかありませんので、サーバを終了すると消えてしまう点にご注意ください。

サーバからデータを取得する

Reactに話を戻します。 このWebアプリは親ComponentであるToDoComponentでStateの面倒を見ていますので、ここからはsrc/todo.jsを編集していくことになります。まずはサーバからデータを取得し表示するところを実装してみます。

class ToDo extends Component {
  constructor(props){
    super(props);
    this.state = {
      todo: [],
      isLoad: false,  //追加
      isError: false  //追加
    };
    this.handleClick  = this.handleClick.bind(this);
    this.handleRemove = this.handleRemove.bind(this);
  }

  componentWillMount(){
    this.fetchAPI();
  }

  fetchAPI(){
    fetch('http://localhost:3001/get')
      .then( res => res.json() )
      .then(
        res => {
          this.setState({
            todo: res.data,
            isLoad: true
          });
        },
        res => {
          this.setState({ isError: true });
        },
      );
  }

  // (snip)

  render() {
    if(this.state.isError){
      return( <div>API Request Error</div> );
    }
    else if(!this.state.isLoad){
      return( <div>...Loading now</div> );
    }
    return(
      <div>
        <ToDoCreate onClick={this.handleClick} />
        <ToDoList data={this.state.todo} remove={this.handleRemove}/>
      </div>
    );
  }
}

変更点のみ掲載しています。

基本的な作りは以前やったサンプルと同じですね。 通信中かどうか、エラーが発生したかどうかをStateで管理し、render()で表示を切り替えます。

初回のAPIへのリクエストはComponentがDOMツリーに追加される前に実行されるcomponentWillMount()内で行います。実際のリクエストはfetchAPI()で行いうまく行けばsetStateでStateを更新、何らかのエラーが発生すればisErrorのフラグを立てるというわけです。

詳細は過去の記事を参照ください。 blog.katsubemakito.net

サーバにデータを保存する

お次は新規にToDoを作成した際に、サーバへ保存します。ここでもsrc/todo.jsを編集します。

  saveAPI(value){
    let method  = "POST";
    let headers = {'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'};
    let body    = "data=" + JSON.stringify(value);

    fetch('http://localhost:3001/set', {method, headers, body})
      .then( res => res.json() )
      .then(
        res => {
          if(res.status === false){
            this.setState({isError:true})
          }
          else{
            this.fetchAPI();
          }
        },
        res => {
          this.setState({isError:true})
        }
      );
  }

  handleClick(value){
    this.saveAPI({name:value});
  }

変更点のみ掲載しています。 ここでのポイントはPOSTメソッドで通信している点と、通信が成功した際にもう一度fetchAPIでリストを取得している点でしょうか。

POSTメソッドの場合は引数が追加されるだけで大きく処理は変わりません。

fetch('http://localhost:3001/set', {method, headers, body})

これまでは handleClick() で新規データをStateに保存する処理を行っていましたが、ここではサーバにすべて任せてしまいましたので、1行だけになっています。ここは一長一短ありますので、Stateを更新してからサーバとの通信を行うようにした方がユーザーのストレスは低くなりますが、通信処理がエラーとなった場合に巻き戻ってしまうリスクもあります。

  handleClick(value){
    this.saveAPI({name:value});
  }

サーバからデータを削除する

最後にデータの削除です。src/todo.jsを編集します。

  removeAPI(id){
    fetch(`http://localhost:3001/remove/${id}`)
      .then( res => res.json() )
      .then(
        res => {
          if(res.status === false){
            this.setState({isError:true})
          }
          else{
            this.fetchAPI();
          }
        },
        res => {
          this.setState({ isError: true });
        },
      );
  }

  handleRemove(e){
    let id = Number( e.currentTarget.getAttribute("data-id") );
    this.removeAPI(id);
  }

変更点のみ掲載しています。 これまでの応用ですね。ここではGETメソッドでリクエストを送り、成功すればfetchAPI()を呼んでリスを最新の状態に更新します。

データ保存も同様ですが、削除する度にデータ全件を取ってくるのは非常に効率が悪いので、このあたりは製品を開発する際には最適化したいところですねw 今回は手を抜きましたw

まとめ

まだまだ改善点はありますが、それっぽい動きをする物ができたと思います。 Reactが良いのはComponentをわけることで、仕事も明確に別れる点ですね。修正したい場合に作業箇所がすぐにわかりますし、それによる影響範囲も限られる点も開発しやすいのではないかと。

最終的なコード

github.com