Rails + React + AjaxでCRUDのサンプルプロジェクト [Hello World]
React初心者が公式サイトで基礎を学んだ後に作るReact + AjaxによるCRUD(作成/読み込み/更新/削除)のサンプルプロジェクトです。
Twitterのように「つぶやき」を投稿可能で編集/削除もできます。
DEMO
https://www.petitmonte.com/rails-demo/react_crud
ソース一式
https://github.com/TakeshiOkamoto/mpp_react_crud
※学習用の為、ライセンスはパブリックドメイン
目次
1. プロジェクトの作成
2. 各パッケージをインストール
3. Bootstrapの導入
4. config/application.rb
5. データベース設定
6. モデルの作成
7. db/seeds.rb
8. マイグレーション
9. コントローラー/ビューの作成
10. config/routes.rb
11. 各コード(ビュー/コントローラー/モデル/JSX)
12. 最後に
1. プロジェクトの作成
これ以下はソース一式からではなく、一からプロジェクトを作成する方の為の解説です。※各コードの解説もあります。
新規プロジェクトに「React」を追加してプロジェクトを作成する。
cd ~/ mkdir myapp cd myapp rails new . --webpack=react --skip-turbolinks --skip-action-mailer --skip-action-mailbox --skip-active-storage --skip-test -d mysql
実行後、app/javascript/packs/hello_react.jsxのファイルが作成されるので「react_crud.jsx」の名称に変更する。
2. 各パッケージをインストール
次のyarnコマンドを実行します。
// ReactをIE9/10/11に対応させる yarn add react-app-polyfill // 日付、時刻の操作(日本語対応) yarn add date-fns // FormDataをIEに対応させる yarn add formdata-polyfill
3. Bootstrapの導入
// Gemfileに以下を追加してbundleする gem 'bootstrap', '4.6.1' // 手動で削除 app/assets/stylesheets/application.css // 手動で作成 app/assets/stylesheets/application.scss application.scssに次のコードをコピペする /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's * vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_tree . *= require_self */ @import "bootstrap"
4. config/application.rb
タイムゾーンの設定。
class Application < Rails::Application ・・・省略 ・・・ config.time_zone = 'Asia/Tokyo' ・・・省略 ・・・ end
5. データベース設定
6. モデルの作成
// メイン用 bin/rails g model react_crud_data name:string comment:text // バックアップ用 bin/rails g model react_crud_data_bk name:string comment:text
7. db/seeds.rb
テーブルの初期データの設定です。
# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
#
# Examples:
#
# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
# Character.create(name: 'Luke', movie: movies.first)
ReactCrudDatum.create(id:1, name: 'プチモンテ', comment: 'Ubuntu16.04にLazarusをインストールする [RADによる開発環境]',
created_at: '2020-02-26 13:37:12', updated_at: '2020-02-26 13:37:12')
ReactCrudDatum.create(id:2, name: 'プチモンテ', comment: '青色申告決算書 & 仕訳帳システム',
created_at: '2020-02-27 20:00:12', updated_at: '2020-02-27 20:00:12')
ReactCrudDatum.create(id:3, name: 'プチモンテ', comment: 'Rails6プロジェクトの各種初期設定',
created_at: '2020-02-28 05:30:12', updated_at: '2020-02-28 05:30:12')
ReactCrudDatum.create(id:4, name: 'プチモンテ', comment: 'NginxにRoundcubeのWebメールシステムを導入する [CentOS]',
created_at: '2020-03-01 12:45:56', updated_at: '2020-03-01 12:45:56')
ReactCrudDatum.create(id:5, name: 'プチモンテ', comment: 'ネットワークカメラを用いた顔認識及び人物特定システムの構築 [防犯カメラの自作]',
created_at: '2020-03-02 18:22:08', updated_at: '2020-03-02 18:22:08')
ReactCrudDatum.create(id:6, name: 'プチモンテ', comment: 'Android App Bundle(*.aab)でゲームをアップロード [Unity]',
created_at: '2020-03-03 22:28:00', updated_at: '2020-03-03 22:28:00')
ReactCrudDatum.create(id:7, name: 'プチモンテ', comment: '収縮と膨張によるノイズ除去のサンプルコード(2値画像用)',
created_at: '2020-03-03 23:30:11', updated_at: '2020-03-03 23:30:11')
ReactCrudDatum.create(id:8, name: 'プチモンテ', comment: 'JavaScriptでC/C++コードを実行してネイティブアプリのように高速にする [WebAssembly]',
created_at: '2020-03-03 23:47:52', updated_at: '2020-03-03 23:47:52')
ReactCrudDataBk.create(id:1, name: 'プチモンテ', comment: 'Ubuntu16.04にLazarusをインストールする [RADによる開発環境]',
created_at: '2020-02-26 13:37:12', updated_at: '2020-02-26 13:37:12')
ReactCrudDataBk.create(id:2, name: 'プチモンテ', comment: '青色申告決算書 & 仕訳帳システム',
created_at: '2020-02-27 20:00:12', updated_at: '2020-02-27 20:00:12')
ReactCrudDataBk.create(id:3, name: 'プチモンテ', comment: 'Rails6プロジェクトの各種初期設定',
created_at: '2020-02-28 05:30:12', updated_at: '2020-02-28 05:30:12')
ReactCrudDataBk.create(id:4, name: 'プチモンテ', comment: 'NginxにRoundcubeのWebメールシステムを導入する [CentOS]',
created_at: '2020-03-01 12:45:56', updated_at: '2020-03-01 12:45:56')
ReactCrudDataBk.create(id:5, name: 'プチモンテ', comment: 'ネットワークカメラを用いた顔認識及び人物特定システムの構築 [防犯カメラの自作]',
created_at: '2020-03-02 18:22:08', updated_at: '2020-03-02 18:22:08')
ReactCrudDataBk.create(id:6, name: 'プチモンテ', comment: 'Android App Bundle(*.aab)でゲームをアップロード [Unity]',
created_at: '2020-03-03 22:28:00', updated_at: '2020-03-03 22:28:00')
ReactCrudDataBk.create(id:7, name: 'プチモンテ', comment: '収縮と膨張によるノイズ除去のサンプルコード(2値画像用)',
created_at: '2020-03-03 23:30:11', updated_at: '2020-03-03 23:30:11')
ReactCrudDataBk.create(id:8, name: 'プチモンテ', comment: 'JavaScriptでC/C++コードを実行してネイティブアプリのように高速にする [WebAssembly]',
created_at: '2020-03-03 23:47:52', updated_at: '2020-03-03 23:47:52')
8. マイグレーション
// 各テーブルの作成 bin/rails db:migrate // 各テーブルの初期データの作成 bin/rails db:seed
9. コントローラー/ビューの作成
SPA(Single Page Application)なので1つだけです。
bin/rails g controller react_crud_data index
10. config/routes.rb
この設定はAjaxから呼び出すAPIのURLともなります。
Rails.application.routes.draw do # ルート root to: 'react_crud_data#index' get 'react_crud_data/index' get 'react_crud_data/new', to: 'react_crud_data#new', as: 'new_react_crud_data' post 'react_crud_data', to: 'react_crud_data#create' put 'react_crud_data/:id', to: 'react_crud_data#update' delete 'react_crud_data/:id', to: 'react_crud_data#destroy' end
11. 各コード(ビュー/コントローラー/モデル/JSX)
11.1 ビュー
[app/views/layouts/application.html.erb]
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title><%= "Rails + React + AjaxでCRUDのサンプルプロジェクト" %></title>
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1" />
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_pack_tag 'application' %>
<%# react_crud.jsxの読み込み %>
<%= javascript_pack_tag 'react_crud' %>
</head>
<body>
<div class="navbar navbar-expand-md navbar-light bg-primary">
<div class="navbar-brand text-white"><%= "Rails + React + AjaxでCRUDのサンプルプロジェクト" %></div>
<ul class="navbar-nav ml-auto">
<li class="nav-item"><%= link_to '全て初期化', new_react_crud_data_path, class: 'nav-link',style: "color:#fff;" %></li>
</ul>
</div>
<div class="container">
<%= yield %>
</div>
<br>
<nav class="container bg-primary p-2 text-center">
<div class="text-center text-white">
React "CRUD" Sample
</div>
<div class="text-center text-white">
Takeshi Okamoto wrote the code.
</div>
<p></p>
</nav>
</body>
</html>
[app/views/react_crud_data/index.html.erb]
<div id="root"></div>
11.2 コントローラー
[app/controllers/react_crud_data_controller.rb]
class ReactCrudDataController < ApplicationController
before_action :set_datum, only: [:update, :destroy]
# 英語のdataは複数形、datumは単数形
def index
@data = ReactCrudDatum.all.order(updated_at: "DESC")
respond_to do |format|
# HTML用
format.html
# JSON用
format.json { render json: @data}
# JSONは次のような形式となる
#
# [
# {"id":1,"name":"プチモンテ"}
# {"id":2,"name":"プチラボ"}
# {"id":3,"name":"@ゲーム"}
# ]
end
end
# 全て初期化
def new
ActiveRecord::Base.transaction do
# 高速削除
ActiveRecord::Base.connection.execute("TRUNCATE TABLE react_crud_data;")
# 高速挿入
ActiveRecord::Base.connection.execute("INSERT INTO react_crud_data SELECT * FROM react_crud_data_bks;")
end
redirect_to root_path
end
def create
@datum = ReactCrudDatum.new(datum_params)
respond_to do |format|
if @datum.save
format.json { render json: {registration: "Ajaxによるデータの登録が成功しました。",
id: @datum.id, name: @datum.name, comment: @datum.comment, updated_at: @datum.updated_at} }
else
format.json { render json: {registration: "Ajaxによるデータの登録が失敗しました。",
id: "error"} }
end
end
end
def update
respond_to do |format|
if @datum.update(datum_params)
format.json { render json: {registration: "Ajaxによるデータの更新が成功しました。"} }
else
format.json { render json: {registration: "Ajaxによるデータの更新が失敗しました。"} }
end
end
end
def destroy
respond_to do |format|
if @datum.destroy
format.json { render json: {registration: "Ajaxによるデータの削除が成功しました。"} }
else
format.json { render json: {registration: "Ajaxによるデータの削除が失敗しました。"} }
end
end
end
private
def set_datum
@datum = ReactCrudDatum.find(params[:id])
end
def datum_params
# 送信側のJSONの形式を確認する事!
params.require(:datum).permit(:name, :comment)
end
end
11.3 モデル
[app/models/react_crud_datum.rb]
class ReactCrudDatum < ApplicationRecord
validates :name, length: { maximum: 20 }, presence: true
validates :comment, length: { maximum: 140 }, presence: true
end
11.4 JSX ※React
[app/javascript/packs/react_crud.jsx]
RailsのCSRFトークンにも対応しています。未対応だとAjaxからRailsのAPIを呼び出すと「422 (Unprocessable Entity)」のエラーが表示されて動作しません。
// IE9,10,11対策用
import "react-app-polyfill/ie9"
import 'react-app-polyfill/stable'
// React
import React from 'react'
import ReactDOM from 'react-dom'
import PropTypes from 'prop-types'
// 日時操作
import {format} from 'date-fns'
import ja from 'date-fns/locale/ja'
// IEのFormData対策用
import 'formdata-polyfill'
class ReactCurdComponent extends React.Component {
// ---------------------
// コンストラクタ
// ---------------------
constructor(props) {
super(props);
this.state = {
error: null, // エラー
isLoaded: false, // ローディングフラグ
items: [], // アイテム
mode: [], // アイテムのモード(表示/編集)
name: '', // 投稿 - 名前
comment: '', // 投稿 - コメント
status: 'ここに「Ajax」に関するメッセージが表示されます。'
};
}
// ---------------------
// Ajax通信(送信用)
// ---------------------
run_ajax(method, url, data){
fetch(url,
{
method: method,
body: JSON.stringify(data),
headers:{
// JSON
'Content-Type': 'application/json',
// CSRFトークン
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
})
.then(res => res.json())
.then(
(result) => {
this.setState((state) => {
state.status = "サーバーからのメッセージ(" +
format(new Date(), 'yyyy年MM月dd日(iiiii) HH:mm:ss', {locale: ja}) +") :" +
result.registration;
// 新規登録時のみIDなどが返却される
if(result.id){
// 失敗
if(result.id == "error"){
return {status: state.status}
// 成功
}else{
// 先頭にアイテムを追加する
state.items.unshift({id: result.id,
name: result.name,
comment: result.comment,
updated_at: result.updated_at}
);
state.mode.unshift(false)
return {status: state.status, items: state.items, mode:state.mode}
}
// 更新/削除
}else{
// 更新/削除のエラー制御は行っていないので各自で。
return {status: state.status}
}
});
},
(error) => {
this.setState((state) => {
state.status = error.message;
return {status: state.status}
});
}
)
.catch((error) => {
this.setState((state) => {
state.status = error.message;
return {status: state.status}
});
}
);
}
// ---------------------
// イベント(独自)
// ---------------------
// 投稿 - 名前
handleNameChange(event){
const value = event.target.value;
this.setState((state) => {
state.name = value;
return {name: state.name}
});
}
// 投稿 - コメント
handleCommentChange(event){
const value = event.target.value;
this.setState((state) => {
state.comment = value;
return {comment: state.comment}
});
}
// 表示モード/編集モードの切り替え
handleModeChange(index, event) {
this.setState((state) => {
state.mode[index] = !state.mode[index];
return {mode: state.mode}
});
event.preventDefault();
}
// データの登録
handleInsert(event){
this.setState((state) => {
if (state.name && state.comment){
// Ajax
this.run_ajax("POST",
"http://localhost:3000/react_crud_data/" ,
{datum: {name: state.name, comment: state.comment}}
);
state.name = '';
state.comment = '';
return {items: state.items}
}
});
event.preventDefault();
}
// データの更新
handleUpdate(index, id, event){
// フォームデータ
// ※<input onChange={} />で状態管理を行うとキャンセルができないので<input defaultValue={} />とFormDataを使っています。
const form_data = new FormData(event.target);
this.setState((state) => {
const txt_name = form_data.get('txt_name');
const txt_comment = form_data.get('txt_comment');
if (
(txt_name && txt_comment) &&
(!(state.items[index].name == txt_name &&
state.items[index].comment == txt_comment))
){
state.items[index].name = txt_name;
state.items[index].comment = txt_comment;
state.items[index].updated_at = new Date();
// Ajax
this.run_ajax("PUT",
"http://localhost:3000/react_crud_data/" + id ,
{datum: {name: txt_name, comment: txt_comment}}
);
return {items: state.items}
}
});
// 表示モードに変更する
this.handleModeChange(index, event);
}
// データの削除
handleDelete(index, id, event) {
this.setState((state) => {
state.items.splice(index, 1);
state.mode.splice(index, 1);
// Ajax
this.run_ajax("DELETE",
"http://localhost:3000/react_crud_data/" + id ,
{}
);
return {items: state.items, mode:state.mode}
});
event.preventDefault();
}
// ---------------------
// イベント(React)
// ---------------------
// データの読み込み
componentDidMount() {
// JSONデータの取得
fetch("http://localhost:3000/react_crud_data/index.json")
.then(res => res.json())
.then(
(result) => {
// モードの初期化(全て表示モード)
const mode = Array(result.length).fill(false);
this.setState({
isLoaded: true,
items: result,
mode: mode
});
},
(error) => {
this.setState({
isLoaded: true,
error
});
}
)
}
// ---------------------
// メイン
// ---------------------
render() {
const { error, isLoaded, items, mode} = this.state;
// エラー
if (error) {
return <div>Error: {error.message}</div>;
// ローディング
} else if (!isLoaded) {
return <div>Loading...</div>;
// 正常動作
} else {
return (
<div>
<p />
<div className="fixed-bottom bg-dark text-white" style={{opacity: 0.55}}>
<span> </span>
<span>{this.state.status}</span>
</div>
<h3>投稿</h3>
<p />
<form onSubmit={this.handleInsert.bind(this)}>
<input type="text" value={this.state.name} name="txt_name" className="form-control" placeholder="名前" onChange={this.handleNameChange.bind(this)} />
<textarea value={this.state.comment} name="txt_comment" className="form-control" rows="5" placeholder="コメントを入力します。" onChange={this.handleCommentChange.bind(this)} />
<input type="submit" value="登録" className="btn btn-primary" />
</form>
<p />
<h3>一覧</h3>
<p />
<div className="card-columns">
{items.map((item,index) => {
// 表示モード
if (!mode[index]){
return(
<div className="card" key={index}>
<div className="card-header">
{item.name} <br />{format(new Date(Date.parse(item.updated_at)), 'yyyy年MM月dd日(iiiii) HH:mm:ss', {locale: ja})}
</div>
<div className="card-body">
{item.comment}
<br />
<br />
<form>
<div style={{textAlign:"right"}}>
<input type="submit" value="編集" className="btn btn-primary" onClick={this.handleModeChange.bind(this,index)} />
<input type="submit" value="削除" className="btn btn-danger" onClick={this.handleDelete.bind(this,index,item.id)} />
</div>
</form>
</div>
</div>
);
// 編集モード
}else{
return(
<div className="card" key={index}>
<form onSubmit={this.handleUpdate.bind(this,index,item.id)}>
<div className="card-header">
<input type="text" defaultValue={item.name} name="txt_name" className="form-control" />
</div>
<div className="card-body">
<textarea defaultValue={item.comment} name="txt_comment" className="form-control" rows="5" />
</div>
<div style={{textAlign:"right"}}>
<input type="submit" value="キャンセル" className="btn btn-secondary" onClick={this.handleModeChange.bind(this,index)} />
<input type="submit" value="更新" className="btn btn-primary" />
</div>
<p />
</form>
</div>
);
}
})}
</div>
</div>
);
} // end if
} // end render
}
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(
<ReactCurdComponent />,
document.getElementById('root')
)
})
これで、プロジェクトが完成です。動作しない場合はソース一式やRails6プロジェクトの各種初期設定を参照して下さい。
12. 最後に
このサンプルプロジェクトはReactで初めての作品です。
動作確認はChrome、FireFox、Microsoft Edge、IE11です。恐らくマックさんのブラウザも動作するはずです。
個人的にはJavaScriptは好きな部類なのでReact、Vue.jsなどでしばらく遊びます。また、今回のサンプルはエラー処理は完全ではないので、運用する場合は各自で対処して下さい。
関連記事
| 前の記事: | ReactをIE9/10/11に対応させる [Rails6] |
| 次の記事: | Uncaught TypeError: $(...).toast is not a function [Rails/bootstrap] |
プチモンテ ※この記事を書いた人
![]() | |
![]() | 💻 ITスキル・経験 サーバー構築からWebアプリケーション開発。IoTをはじめとする電子工作、ロボット、人工知能やスマホ/OSアプリまで分野問わず経験。 画像処理/音声処理/アニメーション、3Dゲーム、会計ソフト、PDF作成/編集、逆アセンブラ、EXE/DLLファイルの書き換えなどのアプリを公開。詳しくは自己紹介へ |
| 🎵 音楽制作 BGMは楽器(音源)さえあれば、何でも制作可能。歌モノは主にロック、バラード、ポップスを制作。歌詞は抒情詩、抒情的な楽曲が多い。楽曲制作は🔰2023年12月中旬 ~ | |









