🙆

React + Rails API モードで基本的な CRUD アプリを作ってみた (フロントエンド編 その2)

8 min read

はじめに

世は大 SPA 時代!!
「そろそろフロントもやっておくか」ということでReact の公式チュートリアルをやりました。

が、実際のアプリケーションではバックエンドとAPIレベルで通信することは必須。
簡単な CRUD 機能を持つバックエンドをRailsの API モードを使って実装し、はじめての SPA に挑戦してみることにしました。

初心者なので至らない部分も沢山あると思いますが、「Rails API + React フロントエンド」という実装についてイメージできるようになっていると思います。
今回は最小限の構成(モデル1つ、コントローラ1つ)ですが、基礎を抑えておけばスケールすることは十分可能だと思います。

それではやっていきましょう🙋‍♂️

No Title
1 バックエンド編
2 フロントエンド編 その1
3 フロントエンド編 その2 ← 今ここ
4 その他考察編

作ったアプリ

ポストを投稿できるだけのシンプルなアプリです。
ポストに対して CRUD 処理を実装していきます。


環境、リポジトリ

Name Version
node.js 12.19.0
React 16.13.1
Ruby 2.6.6
Rails 6.0.3.4
Docker for Mac 2.4.0.0
Name URL
フロントエンド https://github.com/tatsuro-m/react_rails_api_frontend
バックエンド https://github.com/tatsuro-m/react_rails_api_backend

create(新規作成)

新しいポストはトップ画面上部のフォームから作成します。

このフォームの入力値はいわゆる「制御されたコンポーネント」として定義しています。
入力値を state で管理して、フォームの onChange に反応して state を更新する。
フォームの value には state の値を props として渡したものを表示するというやつですね!!

App.js
class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            createFormInputs: {
                title: "",
                content: "",
            },
            posts: [],
        }

CreateForm というコンポーネントを用意して App.js から render します。

CreateForm.js
~~~
    <Button
        variant="contained"
        color="primary"
        endIcon={<SendIcon/>}
        onClick={props.onSubmit}
    >
        CREATE
    </Button>
~~~

ほとんどを省略していますが、 CREATE ボタン押下時に関数を読んでいるのが分かります。
この関数は App.js の、

    handlePostSubmit(e) {
        e.preventDefault();
        const inputValues = Object.values(this.state.createFormInputs);

        if (inputValues.every(value => value)) {
            this.axios.post("/posts", {
                post: this.state.createFormInputs,
            })
                .then(res => {
                    const posts = this.state.posts.slice();
                    posts.push(res["data"]);
                    this.setState({
                        posts: posts,
                        createFormInputs: {
                            title: "",
                            content: "",
                        },
                    });
                })
                .catch(data => {
                    console.log(data)
                });
        }
    }

です!!

空の状態でボタンを押した時にバックエンドへのリクエストが発生しないように簡単なバリデーションを行っています。

const inputValues = Object.values(this.state.createFormInputs);
if (inputValues.every(value => value))

この書き方は初めて知ったのですが便利ですね!
フォームの項目が増えてもこれなら修正不要です。
全てのフォームの中身が入っていないと送信しない仕組みです。

バリデーションに通ったら postリクエストを送ります(投稿の post とややこしいですね、すみません)!

            this.axios.post("/posts", {
                post: this.state.createFormInputs,
            })

state を直接更新するのは React では Guilty です。
複製して使います。

const posts = this.state.posts.slice();

posts の最後尾に作成した post を追加して、フォームの入力もリセットするために空で更新して create も完成です🙋‍♂️

  posts.push(res["data"]);
  this.setState({
      posts: posts,
      createFormInputs: {
          title: "",
          content: "",
      },
  });

バックエンドのログを確認してみてください。

  • パラメータでフォームの内容が送られてきていること
  • SQL が走って Post が登録されていること

が確認できるかと思います。

update(更新)

疲れてきましたがもう少しです、頑張りましょう。
次は update ですね。

詳細のモーダルと同じように、更新用のフォームもそれぞれの post コンポーネントから render させています。
表示の切り替えも state に用意した真偽値で行います。
ただし今回は Material UI が用意してくれている Modal コンポーネントの onClose プロパティ みたいなのはありません。
表示切り替えのロジックは自分で作らなくてはいけません。

とは言っても大したことはありません。
React では簡単に 条件付きレンダー が使えるからです。

class Post extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            modalOpen: false,
            editFormOpen: false,
            editFormInputs: {
                title: "",
                content: "",
            },
        }
        this.handleToggleEditFormOpen = this.handleToggleEditFormOpen.bind(this);
    }

    handleToggleEditFormOpen() {
        this.setState({
            editFormOpen: !this.state.editFormOpen
        });
    }

    render() {
        return (
                {this.state.editFormOpen &&
                <EditForm
                    post={this.props.post}
                    inputs={this.state.editFormInputs}
                    onChange={this.handleInputChange}
                    onSubmit={this.props.onUpdate}
                />
                }
        );

ここでは、 this.state.editFormOpen が true であれば右辺が評価されるという特性を生かしてこのように書いています。

こうすると、

EDIT ボタンを押した時に editFormOpen の真偽値が toggle されて、

フォームが現れます!
あとは UPDATE ボタンが押された時に起動する handler を設定すればOKです!

    handlePostUpdate(id, inputs, e) {
        e.preventDefault();
        const inputValues = Object.values(inputs);

        if (inputValues.every(value => value)) {
            this.axios.patch(`/posts/${id}`, {
                post: inputs
            })
                .then(results => {
                    const posts = this.state.posts.slice();
                    const index = posts.findIndex(post => post["id"] === id);
                    posts.splice(index, 1, results["data"]);

                    this.setState({
                        posts: posts
                    });
                })
                .catch(data => {
                    console.log(data);
                });
        }
    }

create のところと似ているのでざっくり解説にはなりますが、

   const posts = this.state.posts.slice();
   const index = posts.findIndex(post => post["id"] === id);
   posts.splice(index, 1, results["data"]);

この辺はポイントでしょうか。
create と違って最後尾に入れれば良いわけではないので更新した post のインデックス番号を探して、 splice メソッドを使ってピンポイントで更新しています。

バックエンドのログを確認して更新されていることを確認しましょう😊

destroy(削除)

ここまでくれば destroy は楽勝です!
削除したい post の id さえあればリクエストは送れますね!

App.js
    handlePostDelete(id, e) {
        e.preventDefault();
        this.axios.delete(`/posts/${id}`)
            .then(res => {
                const targetIndex = this.state.posts.findIndex(post => {
                    return post["id"] === res["data"]["id"]
                });
                const posts = this.state.posts.slice();
                posts.splice(targetIndex, 1);

                this.setState({
                    posts: posts
                });
            })
            .catch(data => {
                console.log(data);
            });
    }

成功時の処理は update と似ていますね。
インデックス番号を取得して splice で削除しています。

DELETE ボタン押下時にこの関数が呼ばれるように post コンポーネントに props として関数を渡します。
この関数を呼ぶ際、post コンポーネントは自身の id を引数で渡します。

Post.js
  <Button
      size="small"
      variant="contained"
      color="secondary"
      startIcon={<DeleteIcon/>}
      onClick={(e) => this.props.onDelete(this.props.post.id, e)}  // ← ここ!!
  >
      DELETE
  </Button>

バックエンドのログを確認して、削除されていることを確認してみましょう。


ふう。。。
お疲れ様でした!

これで CRUD 処理は実装完了です。
特にReact で至らない部分が多くありますが、今後の課題として持ち帰ります🙌

React + Rails API モードの実装イメージが湧いたでしょうか?
これで本編は終わりですが、次回はおまけとして追加の解説や考察もしたいと思っております。

次回もお楽しみに!!