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

公開:2020/10/19
更新:2020/10/21
8 min読了の目安(約7300字TECH技術記事

はじめに

世は大 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

フロントエンド

雛形構築は、

$ npx create-react-app アプリ名 

でやってしまいます。
rails で言うところの rails new 的な。

まずは API のURLを環境変数で設定しておきましょう。
create-react-app するとデフォルトで dotenv が使えるようになっているらしいです。

プロジェクトルート直下に .env ファイルを追加して、バックエンドのURLを書きます。

REACT_APP_DEV_API_URL=http://localhost:3001

こうすると、

process.env.REACT_APP_DEV_API_URL

で簡単に参照できるみたいです。

すごい。。。!!

コンポーネント構成

メインとなる src 配下は以下のようになっています。

./src/
├── App.css
├── App.js
├── App.test.js
├── components
│   ├── CreateForm.js
│   ├── EditForm.js
│   ├── Post.js
│   └── PostModal.js
├── index.css
├── index.js
├── logo.svg
├── serviceWorker.js
└── setupTests.js

App.js をルートコンポーネントとして、そこからコンポーネントを render していきます。

axios を使う

HTTP 通信を扱えるものであれば何でも良いのですが、今回は axios を使ってみたいと思います。
インストールを済ませておきましょう🙌

$ yarn add axios -D

基本的にバックエンドとやり取りするコンポーネントは最上位の App.js とします。
なので、こちらに axios の設定を書いていきます。

// 省略
class App extends React.Component {
// 省略

        get axios() {
            const axiosBase = require('axios');
            return axiosBase.create({
                baseURL: process.env.REACT_APP_DEV_API_URL,
                headers: {
                    'Content-Type': 'application/json',
                    'X-Requested-With': 'XMLHttpRequest'
                },
                responseType: 'json'
            });
        }
}
// 省略

先ほど設定した URL の環境変数を使っています。
ベースとなる axios インスタンスが返るようにしておくことで、既に URL の設定が終わっているので追加のパスを指定するだけでよくなります。

getter メソッドとして定義しているので同じクラス内から、
this.axios.get('path')

みたいに使えるようになります。
ではそれぞれのアクションについて実装していきましょう。

デザインについて

今回はRails API との接続部分についてフォーカスしたいので、デザインに関してはそれほど突っ込みません。
画像にもある通りカードデザインで1つのポストを表示してみました。

デザインには、 Material UI を使っております。
今回もデザインセンス0の私が作ったアプリを最低限のレベルに押し上げてくれました。
おすすめの UI コンポーネントなので是非使ってみてください!!

index(一覧)

React アプリの初期表示です。
バックエンドからポストの一覧を取ってきて画面に表示します。

このように「初期表示時にバックエンドからデータが欲しい」みたいな時には、いわゆるライフサイクルメソッドを使うと良さそうです。
state とライフサイクル

// 省略
class App extends React.Component {
// 省略

    componentDidMount() {
        this.axios.get('/posts')
            .then(results => {
                console.log(results);
                this.setState({
                    posts: results.data
                });
            })
            .catch(data => {
                console.log(data);
            });
    }
}
// 省略

componentDidMount はコンポーネントがマウントされた時に実行される関数です。
逆に、マウントされなくなった時に起動する componentWillUnmount もあります(使ったことは無いです)🍴

axios は promise インスタンスを返すので、 then で成功時の処理を書きます。
この辺は async 関数 を使っても良かったかもしれません。

返ってきたデータは results["data"] の中に入っていますのでそれを使って state を更新しています。

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            posts: [],
        }
// 省略

posts はポスト全体を配列で管理する state です。

上手くできましたね!!
表示はカード形式で行います。

show(詳細)

地味に悩んだのがここでした。
ポストの詳細を表示したいという要件。

カード内にある DETAIL ボタンを押した時に表示されるようにしたかったです。

別に API にリクエストを投げても良いのですが、今回の場合は無駄になりそうです。。。
全ての post のデータを取得しているので、 posts state から情報を読み出せば問題無いからです。

なのでリクエストは飛ばさないことにしました(バックエンドではアクションを実装していたような、、、適当ですみません😅)

DETAIL ボタンを押すと詳細表示用のモーダルを開くようにしてみました。

モーダルを1つのコンポーネントとして考えます。
それぞれの post コンポーネントは、1つずつ Modal も持っていることになります。
Modal 表示の切り替えは Post コンポーネントが持っている state に与えた真偽値で管理します。

表示状態の ON/OFF を toggle する handler を用意します。

Post.js
class Post extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            modalOpen: false,
// 省略

        handleToggleModalOpen() {
            this.setState({modalOpen: !this.state.modalOpen});
        }

モーダル自体はシンプルな表示だけなので、

PostModal.js
import React from "react";
import Modal from "@material-ui/core/Modal";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";
import {makeStyles} from "@material-ui/core/styles";
import DeleteIcon from "@material-ui/icons/Delete";

function rand() {
    return Math.round(Math.random() * 20) - 10;
}

function getModalStyle() {
    const top = 50 + rand();
    const left = 50 + rand();

    return {
        top: `${top}%`,
        left: `${left}%`,
        transform: `translate(-${top}%, -${left}%)`,
    };
}

const useStyles = makeStyles((theme) => ({
    paper: {
        position: 'absolute',
        width: 500,
        height: 500,
        backgroundColor: "#d9ded9",
        border: '0.5px solid #000',
        boxShadow: theme.shadows[5],
        padding: theme.spacing(2, 4, 3),
    },
}));

function PostModal(props) {
    const classes = useStyles();
    const modalStyle = getModalStyle();

    const body = (
        <div style={modalStyle} className={classes.paper}>
            <h2>{props.post.title}</h2>
            <p>{props.post.content}</p>
            <p>作成日時: {props.post.created_at}</p>
            <p>更新日時: {props.post.updated_at}</p>
            <Grid container>
// 省略
                <Grid item xs={4}>
                    <Button
                        size="small"
                        variant="contained"
                        onClick={props.onClose}
                    >
                        CLOSE
                    </Button>
                </Grid>
            </Grid>
        </div>
    );

    return (
        <Modal
            open={props.open}
            onClose={props.onClose}
        >
            {body}
        </Modal>
    );
}

export default PostModal;

こんな感じでしょうか。
props として 親コンポーネントである post から post の情報を受け取っています。
中身はそれぞれの post の完全な情報なので、 created_at 属性なども含まれることになります。

ここまでやると、

こんな感じのモーダルが表示されます👨‍


想像以上に長くなっているのでフロントエンド編も2つに分けます。。。
次回もお楽しみに!!


参考にさせて頂いた記事