【rails/非同期】コメント機能をReact.jsで実装してみた。その①

【2016/12/17 追記】
RailsとReact（flux）を使ったブログエンジンを開発しました！
何か参考になれば嬉しいです！

Notee
https://github.com/funaota/notee

__________________________

今日も前回に引き続きRails-reactで**React.jsでコメント機能**を実装しました。
いいね機能よりも規模がちょっと大きめで難しかったのでちょっと苦戦しました。
[いいね機能の実装はこちら](http://qiita.com/funao/items/60aa3d7e36cf93e0d832)

今回はそんなコメントの機能の**実装途中で得た新しい知識**と、**実装の手順**をまとめます。

# 実装途中で得た知識

 - 設計段階から**コンポーネントごとに分ける**ようにすると考えやすい
 - Stateに登録するのは、**更新性のあるもののみ**
 - jsx内でコードを書きたいときには`{(()=> {})}`って書けば行けるはず
 - jsx内でループする際には、**ループ内の親要素にはkeyをしっかりと振る**。振らないと警告が出まくります。
 - formで複数のデータをポストしたい場合や、hiddenで隠蔽してparamsを送るときには、**refを使うのが便利**
 - Viewを更新させたいときには、setState関数がマスト
 - React内ではRailsのアソシエーションを使う事が出来ない
 - 小コンポーネントから親コンポーネントへ通知するときには、コールバックを使う

# 実際の実装手順

## まず仕様について

 - CommentのView
    - １つのPOST
        - user名：親コメント
            - user名：子コメント
            - user名：子コメント
            - 子コメント作成ボックス
        - 親コメント作成ボックス
 - Commentの機能一覧
    - 新規コメント作成機能（親）
    - 新規コメント作成機能（子）
    - コメント削除機能
 - Commentの持つカラム
    - post_id
    - user_id
    - reply_id
    - content
 - Commentのアソシエーション
    - Post　has_many　Comments
    - Comment　has_many　reply_Comments
    - Comment　belongs_to　Post
    - Comment　belongs_to　User

って感じです。
これもfacebookのコメント機能をイメージしてもらえれば大丈夫です。
編集機能がないので、そこは機能的に劣ってます笑

## ①Componentに分ける

まず今回の場合はこのように分けました（もっと綺麗に分けられるかも…笑）

 - CommentsBox（複数のコメントを管理するコンポーネント）
    - Comment（１つのコメントを管理するコンポーネント）
    - CommentForm（親と子の新規コメント作成を管理するコンポーネント）

## ②親のコンポーネントを構成して行く

今回の場合は、Commentsboxです。
### 必要なステートを定義します。

今回は、CommentsのループなのでView側からCommentsをpropsとして渡します。
あと、ついでにどうせurlも絶対使うので渡しちゃいます。

```slim:posts.html.slim
= react_component 'CommentsBox', comments: post.comments,  url: api_comments_url
```

react内でもstateに更新性のあるcommentsのみを登録しておきます。
URLは更新性がないのでpropsから取得する形で十分です。
なるべく、stateを減らす事で管理が簡単になります。

```rb:comment.js.jsx
var CommentsBox = React.createClass({
    getInitialState: function() {
        return {
            comments: this.props.comments
        };
    }
});
```

今回は親コメントのループの中に、子コメントのループがある形をとるので、親と子を分けた配列を作成しておくと便利です。
なので、renderingでreturnしてしまう前に分けた配列を作成します。

```rb:comment.js.jsx
var CommentsBox = React.createClass({
    getInitialState: function() {
        return {
            comments: this.props.comments
        };
    },
    render: function(){

        var child_comments = [];
        var parent_comments = [];

        this.state.comments.map((comment) => {
            if(comment.reply_id){
                child_comments.push(comment);
            }else{
                parent_comments.push(comment);
            }
        });
    }
});
```

ちなみに、今回はreply_idを持っているかどうかを子か親か見分ける材料にしています。なので、この通り作りたい場合には、そのようにcontrollerを実装してください。

### 全体のviewの形を整える

そのあと、任意の形にループを作ります。

```rb:comment.js.jsx

var CommentsBox = React.createClass({
    getInitialState: function() {
        return {
            comments: this.props.comments
        };
    },
    render: function(){

        var child_comments = [];
        var parent_comments = [];

        this.state.comments.map((comment) => {
            if(comment.reply_id){
                child_comments.push(comment);
            }else{
                parent_comments.push(comment);
            }
        });

        return (
            <div>
            // 親のループ
            {parent_comments.map((comment) => {
              return (

                  // keyをしっかり振る！！
                  <div key={comment.id}>

                      // 親のコメント表示部分
                      <Comment />

                      // 子のループ
                      {child_comments.map((child_comment) => {

                          // 子のreply_idが親のidと同じ場合のみ表示
                          if(comment.id == child_comment.reply_id){
                              return(
                                  <div key={child_comment.id} className="rep">
                                    // 子のコメント表示部分
                                    <Comment />
                                  </div>
                              );
                          }
                      })}
                      // 子の新規コメント作成部分
                      <CommentForm />
                  </div>
              );
            })}
              // 親の新規コメント作成部分
              <CommentForm  />
          </div>
        );
    }
});

```

CommentsBoxはだいたい完成です。
あとは、各子コンポーネントにどんなpropsを渡すかだけを記述すれば問題ありません。

## ③次にCommentコンポーネントを構成して行きます。

### 親からpropsを受け取る

冷静にCommentコンポーネントを作りたいので、commentをpropsとして受け取らなければ始まりません。あと、ついでにurlも受け取っちゃいましょう。

先ほどの`<Comment />`の部分に`comment={comment} url={this.props.url}`を書き足しましょう！

```rb
<Comment comment={comment} url={this.props.url} />
```

こんな感じ！
これでCommentコンポーネント内でcommentとurlについてアクセス出来るようになりました。熱い！

### Viewを整える

Commentはコメントの表示とコメントの削除を主な機能とするので、以下の用になります。

```rb:comment.js.jsx
var Comment = React.createClass({

    getInitialState: function() {
        return {};
    },

    render: function(){
        return(
            <p>
                <span>UserName: </span>
                {this.props.comment.content}
                <button onClick={this.deleteComment} className="btn btn-default" type="button">DELETE</button>
            </p>
        );
    }
});
```

これでコメントは表示されるようになりました。

しかし、USER名はこれでは表示されません。railsのアソシエーションをしっかりしているのでslimなどであれば、comment.user.nameでUSER名を取得することが出来ます。（[railsのアソシエーションとかがやってるSQLはこちら](http://qiita.com/funao/items/fc5d14002a9f7a010e96)）

しかし、react内ではこれを使う事が出来ません。なので、別途取得する必要があります。詳細は以下です。

```rb:comment.js.jsx
var Comment = React.createClass({

    getInitialState: function() {
        return {
            name: ""
        };
    },

    getUserName: function(user_id){
        $.ajax({
            url: 'api/get_user',
            type: 'GET',
            dataType: 'json',
            cache: false,
            data: {user_id: user_id},
            success: function(data){

                this.setState({name: data.user.name});

            }.bind(this),
            error: function(xhr, status, err){
                console.error(status, err.toString());
            }.bind(this)
        });

    },

    componentDidMount: function(){
        this.getUserName(this.props.comment.user_id);
    },

    render: function(){
        return(
            <p>
                <span>{this.state.name}:</span>
                {this.props.comment.content}
                <button onClick={this.deleteComment} className="btn btn-default" type="button">DELETE</button>
            </p>
        );
    }
});
```

まず、stateにnameを指定してコメントに対してだれがコメントしたかを保持しておきます。先ほど書いた**更新性のあるもののみStateに登録する**に反している用に見えますが、nameはAPIを通じて、取得するものなので、固定値ではなく、更新取得されるものという解釈でstateに登録しています。

**ちなみに、普通の変数とかにnameを保持して表示しようとするとバグリます笑**

また、名前を取得するタイミングは`componentDidMount`に設定するのが良さそうです。

これで、View側は完成です。
次にDELETE機能を実装していきます。

### DELETE機能を実装する

DELETE機能はほぼいいね機能と時と同じです。

```rb:comment.js.jsx
var Comment = React.createClass({

    getInitialState: function() {
        return {
            name: ""
        };
    },

    getUserName: function(user_id){
        $.ajax({
            url: 'api/get_user',
            type: 'GET',
            dataType: 'json',
            cache: false,
            data: {user_id: user_id},
            success: function(data){

                //処理を書く
                this.setState({name: data.user.name});

            }.bind(this),
            error: function(xhr, status, err){
                console.error(status, err.toString());
            }.bind(this)
        });

    },

    componentDidMount: function(){
        this.getUserName(this.props.comment.user_id);
    },

    deleteComment: function() {
        $.ajax({
            url: this.props.url,
            type:"DELETE",
            dataType: 'json',
            cache: false,
            data: {id: this.props.comment.id},
            success: function() {

                //何らかの処理
                console.log("成功！");

            }.bind(this),
            error: function(xhr, status, err) {
                console.error(this.props.url, status, err.toString());
            }.bind(this)
        });
    },

    render: function(){
        return(
            <p>
                <span>{this.state.name}:</span>
                {this.props.comment.content}
                <button onClick={this.deleteComment} className="btn btn-default" type="button">DELETE</button>
            </p>
        );
    }
});
```

これで完璧！かと思いきや
このままでは、Viewが更新されません。なぜならsetState()が呼ばれていないからです。
削除したコメントを、Viewからも削除するためには、Commentコンポーネントではなくて、CommentsBoxコンポーネントのstateを変更する必要があります。

**ここで結構悩みました！**

### 子要素から親要素に通知する

こういう時には、**コールバック関数を使う**みたいです。（もしくは、fluxという手法を使うみたいです。勉強出来たらまた記事書きます）

参考URL
ReactなComponent同士を連携させたい←（めちゃめちゃ助かりました！）
https://hogehuga.com/post-830/#i-2

まずCommentsBoxの呼び出し関数を修正します。

`<Comment comment={comment} url={this.props.url} onChange={this.changeComments}/>`

このような形で！
そうすると、Commentコンポーネント側でthis.props.onChange()が呼ばれると、CommensBoxコンポーネントのchangeComments()が呼ばれるようになります。

つまり、ajaxが終わったタイミングでthis.props.onChange()をおこない、changeComments()の関数内でthis.state.commentsを更新すればALL OK！になるわけです。

つまり全体をみるとこんな感じです。

```rb:comment.js.jsx

var Comment = React.createClass({

    getInitialState: function() {
        return {
            name: ""
        };
    },

    getUserName: function(user_id){
        $.ajax({
            url: 'api/get_user',
            type: 'GET',
            dataType: 'json',
            cache: false,
            data: {user_id: user_id},
            success: function(data){

                //処理を書く
                this.setState({name: data.user.name});

            }.bind(this),
            error: function(xhr, status, err){
                console.error(status, err.toString());
            }.bind(this)
        });

    },

    componentDidMount: function(){
        this.getUserName(this.props.comment.user_id);
    },

    deleteComment: function() {
        $.ajax({
            url: this.props.url,
            type:"DELETE",
            dataType: 'json',
            cache: false,
            data: {id: this.props.comment.id},
            success: function() {

                //何らかの処理
                console.log("成功！");
                this.props.onChange();

            }.bind(this),
            error: function(xhr, status, err) {
                console.error(this.props.url, status, err.toString());
            }.bind(this)
        });
    },

    render: function(){
        return(
            <p>
                <span>{this.state.name}:</span>
                {this.props.comment.content}
                <button onClick={this.deleteComment} className="btn btn-default" type="button">DELETE</button>
            </p>
        );
    }
});

var CommentsBox = React.createClass({

    getInitialState: function() {
        return {
            comments: this.props.comments,
        };
    },

    changeComments: function(){
        $.ajax({
            url: this.props.url,
            type: 'GET',
            dataType: 'json',
            cache: false,
            data: {id: this.props.post_id},
            success: function(data){

                //処理を書く
                this.setState({comments: data});

            }.bind(this),
            error: function(xhr, status, err){
                console.error(status, err.toString());
            }.bind(this)
        });
    },

    render: function() {

        var child_comments = [];
        var parent_comments = [];

        this.state.comments.map((comment) => {
            if(comment.reply_id){
                child_comments.push(comment);
            }else{
                parent_comments.push(comment);
            }
        });

        return (
          <div>
            {parent_comments.map((comment) => {
              return (
                  <div key={comment.id} >
                      <Comment comment={comment} url={this.props.url} onChange={this.changeComments}/>

                      {child_comments.map((child_comment) => {
                          if(comment.id == child_comment.reply_id){
                              return(
                                  <div key={child_comment.id} className="rep">
                                    <Comment comment={child_comment} url={this.props.url} onChange={this.changeComments} />
                                  </div>
                              );
                          }
                      })}
                      <CommentForm />

                  </div>
              );
            })}
              <CommentForm />
          </div>
        );
    }
});


```

これで、コメントの表示と、コメントの削除機能は実装が終わりました。
長くなってきたので、続きはまた明日にしたいと思います！

あとは、コメント作成機能をつければ完成です。
地味に時間が掛かってしまったので、まとめておきたいと思います笑