Edited at

Rails × React で Rails の scaffold ライクなアプリケーションを実装 - 詳細・編集・更新 -

More than 1 year has passed since last update.

Rails×ReactでRailsのscaffoldで生成されるような簡単なcrudができるアプリケーションを実装したので、備忘録として残しておこうと思います。


アプリケーションの概要

タイトルと本文のあるメッセージをCRUD+絞り込み検索するアプリケーション


やったこと

(1) 環境の構築

(2) メッセージ一覧を表示する。

(3) 新規メッセージを作成する。

(4) メッセージ詳細の表示、編集、更新をする。(本エントリ)

(5) メッセージを削除する、メッセージを検索する。


やってないこと


  • Redux、MobXのステート管理のためのフレームワークの導入

  • テストの実装

  • CSSの実装

  • セキュリティやデータの整合性を確認する実装

  • 用語や実装の解説

用語や実装の解説については、参考にさせていただいたサイトのリンクを各実装毎に貼っておりますので、そちらをご確認下さい。

参考サイトの作成者の皆様ありがとうございます。

【ご一読いただくにあたって】

・間違いがありましたらコメントにて教えて下さい。(まだまだ勉強中です。)


事前準備


一覧表示をscaffoldに寄せる

Railsのscaffoldで生成される一覧の表示は以下のようなものです。

List.jsの表示をこれに寄せるために以下のように修正します。


./react_sample/frontend/src/List.js

   ~ render内のみ修正なので上は省略します ~

render(){
const { messages } = this.state;
return(
<div>
// tableタグを追加する
<table>
<thead>
<tr>
<th>title</th>
<th>content</th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{ messages.map((message) => {
return (
<tr key={ message.id }>
<td>{ message.title }</td>
<td>{ message.content }</td>
<td>show</td>
<td>edit</td>
<td>destroy</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
}
export default List;


そうすると表示は以下のようになったと思います。

若干違いますが、これで進めていきます。


メッセージの詳細

詳細画面まわりのフローは以下のような流れをイメージしています。

1. [show] をクリックするとメッセージのタイトルと詳細が表示される。(詳細画面)

2. 詳細画面には[Edit]があり、クリックすると編集画面が表示される。

画面
URL

詳細画面
http://localhost:4000/message/id


React


[show] をクリックするとメッセージのタイトルと詳細を表示する

まずは、ルーティングを確認するためにShow.jsを作成します。


Show.jsを作成


./react_sample/frontend/src/Show.js

import React, { Component } from 'react';

class Show extends Component {
render() {
return (
<div>
<p>Show Message</p>
</div>
);
}
}

export default Show;



ルーティングを設定

次に、ルーティングを設定します。

今回は、新規登録画面の時と違い、idをキーにして、詳細画面を表示するので、パラメータにidをセットします。

App.jsList.jsを以下のように修正します。


./react_sample/frontend/src/App.js

~ 他のimportは省略します。 ~

import Show from './Show'; // Showコンポーネントのインポートを追加

const App = () => (
<Router>
<div>
<AppHeader />
<Switch>
<Route exact path='/new' component={ New } />
// pathが '/message/:id' のときにShowコンポーネントが表示されるようにします。
<Route exact path='/message/:id' component={ Show }/>
<Route exact path='/' component={ List }/>
</Switch>
<AppFooter />
</div>
</Router>
)


この時点でlocalhost:4000を確認してみます。(イメージは省略します。)

URLにidが渡されていて、Show Messageが表示されていれば成功です。


idからデータを取得


Rails(APIサーバ)側でのデータの取得

それでは、次にRails(APIサーバ)を修正してidが渡ってきたときに、該当するメッセージだけをjsonで返すようにします。

今回の記事のメインはReact側なので、Rails側の解説は省略したいと思います。


config/routes.rb

Rails.application.routes.draw do

resources :messages, only: [:index, :create, :show], format: 'json'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end


app/controllers/messages_controller.rb

class MessagesController < ApplicationController

before_action :set_message, only: [:show]

def index
messages = Message.all
render json: messages
end

def create
message = Message.new(message_params)
message.save!
end

def show
render json: @message
end

private

def set_message
@message = Message.find(params[:id])
end

def message_params
params.require(:message).permit(:title, :content)
end
end


http://localhost:3000/messages/1をURLに入力したときにjsonMessage.idが 1 のデータが返ってくることを確認します。


React側でのデータの受け取り

次にReact側での実装です。

Show.jsを、パラメータでidが渡ってきたら、「(1)取得する/(2)Railsに渡す/(3)Railsからjsonを受けるとる/(4)受け取ったjsonデータを表示する」というように修正します。


./react_sample/frontend/src/Show.js

import React, { Component } from 'react';

import { Link } from 'react-router-dom';

// RailsAPIのURL
const REQUEST_URL_SHOW = 'http://localhost:3000/messages/';

class Show extends Component {
constructor(props) {
super(props)
// this.props.match.params.idでパラメータのidを取得する
const message_id = parseInt(this.props.match.params.id, 10) // (1)
// message_idをstateにセットする
this.state = {
id: message_id,
message: ''
};
}
// New.jsと同様にrenderされる前にfetchDataを走らせる
componentWillMount() {
this.fetchData()
}

// RailsAPIをたたく
fetchData() {
fetch(REQUEST_URL_SHOW + this.state.id) // (2)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
})
.then((response) => response.json()) // (3) jsonへパースする
// 返ってきたデータを this.state.message にセットする
// setState することでレンダリングされる
.then((responseData) => {
this.setState({
message: responseData // (4)
})
})
.catch((err) => {
console.error(err);
});
}

render(){
const { message } = this.state;
return(
<div key={message.id}>
<div>
<b>Title</b>
<div>{ message.title }</div>
</div>
<div>
<b>Content</b>
<div>{ message.content }</div>
</div>
// 編集画面へのリンク ( リンク先はこの段階では未設定 )
<div><Link to={ '/message/' + message.id + /edit' }>edit</Link></div>
</div>
);
}
}

export default Show;


それでは挙動を確認します。



(gifではレンダリングの挙動がカクカクしていますが、ブラウザ上ではカクカクしていないと思います...)

以上で、詳細画面の表示は完了です。

続けて、メッセージの編集/更新を実装していきます。


メッセージの編集/更新

編集/更新のフローは以下のような流れをイメージしています。

1. [Edit] をクリックすると編集フォームが表示される。

2. [Update] をクリックするとメッセージが更新され、一覧画面が表示される。

画面
URL

編集画面
http://localhost:4000/message/:id/edit


React


[Edit]をクリックとEdit.jsのEditコンポーネントが表示されるようにする


Edit.jsを作成

Edit.jsではShow.jsと同様にパラメータでidを渡して、「(1)取得する/(2)Railsに渡す/(3)Railsからjsonを受けるとる/(4)受け取ったjsonデータを表示する」というように修正します。


./react_sample/frontend/src/Edit.js

import React, { Component } from 'react';

const REQUEST_URL_EDIT = 'http://localhost:3000/messages/';

class Edit extends Component {
constructor(props) {
super(props)
const message_id = parseInt(this.props.match.params.id, 10)
this.state = {
id: message_id,
title: '',
content: ''
};
}

componentWillMount() {
this.fetchData()
}

fetchData() {
fetch(REQUEST_URL_EDIT + this.state.id)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
})
.then((response) => response.json())
.then((responseData) => {
this.setState({
title: responseData.title,
content: responseData.content
})
})
.catch((err) => {
console.error(err);
});
}

handleInputValue = (event) => {
const field = event.target.name;
this.state[field] = event.target.value;
this.setState({
[field]: this.state[field]
});
}

render(){
const { title, content } = this.state;
console.log(this.props.title );
return(
<div>
<p>Edit Message</p>
<div>
<form>
<label>title:</label>
<input type="text" name="title" value={ title } onChange={ this.handleInputValue } />
<label>content:</label>
<textarea name="content" value={ content } onChange={ this.handleInputValue } />
<input type="submit" value="Update" />
</form>
</div>
</div>
);
}
}

export default Edit;



ルーティングを設定

続けて、App.jsList.jsを修正します。


./react_sample/frontend/src/App.js

~ 省略 ~

import Edit from './Edit';

// ReactRouter を使ったコンポーネントの定義
const App = () => (
<Router>
<div>
<AppHeader />
<Switch>
<Route exact path='/new' component={ New } />
<Route exact path='/message/:id' component={ Show }/>
// EditコンポーネントのRouteを追加する
<Route exact path='/message/:id/edit' component={ Edit }/>
<Route exact path='/' component={ List }/>
</Switch>
<AppFooter />
</div>
</Router>
)
~ 省略 ~



./react_sample/frontend/src/List.js

~ 省略 ~

render(){
const { messages } = this.state;
return(
<div>
<table>
<thead>
<tr>
<th>Title</th>
<th>Content</th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{ messages.map((message) => {
return (
<tr key={ message.id }>
<td>{ message.title }</td>
<td>{ message.content }</td>
<td><Link to={ '/message/' + message.id }>Show</Link></td>
// Editへのリンクを追加する
<td><Link to={ '/message/' + message.id + '/edit' }>Edit</Link></td>
<td>destroy</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
~ 省略 ~

ここまでで編集の機能は実装できました。

それでは次に更新の機能を実装していきます。

まずは、Edit.jshandleSubmitを追加します。


./react_sample/frontend/src/Edit.js

~ 省略 ~

// [submit]がクリックされた時の処理を追加する
handleSubmit = (event) => {
event.preventDefault();
fetch(REQUEST_URL_EDIT + this.state.id, {
method: 'PUT',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: {
title: this.state.title,
content: this.state.content
}
})
})
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
this.props.history.push('/');
})
.catch((err) => {
console.error(err);
});
}

render(){
const { title, content } = this.state;
return(
<div>
<p>Edit Message</p>
<div>
<form onSubmit={ this.handleSubmit }>
~ 省略 ~
</form>
</div>
</div>
);
}
}



Rails


メッセージの更新(APIサーバ)


config/routes.rb

Rails.application.routes.draw do

resources :messages, only: [:index, :create, :show, :update], format: 'json'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end


app/controllers/messages_controller.rb

class MessagesController < ApplicationController

before_action :set_message, only: [:show, :update]
~ 省略 ~
def update
@message.update!(message_params)
end
~ 省略 ~
end

Rails側の実装を完了したら、localhost:4000を確認してみます。

以下のような挙動になっていれば成功です。


Form.jsの共通化

New.jsEdit.jsで入力フォームを実装しているのですが、共通の部分が多いので共通化します。

ここでReactのpropsという概念が出てきます。

New.jsEdit.jsをそれぞれ以下のように修正します。


./react_sample/frontend/src/New.js

import React, { Component } from 'react';

import Form from './Form';
~ 省略 ~
render() {
const { title, content } = this.state;
return (
<div>
<p>New Message</p>
<Form
submitValue='Submit'
title={ title }
content={ content }
parentSubmit={ this.handleSubmit }
parentInputValue={ this.handleInputValue } />
</div>
);
}
~ 省略 ~


./react_sample/frontend/src/Edit.js

import React, { Component } from 'react';

import Form from './Form';
~ 省略 ~
render(){
const { title, content } = this.state;
return(
<div>
<p>Edit Message</p>
<Form
submitValue='Update'
title={ title }
content={ content }
parentSubmit={ this.handleSubmit }
parentInputValue={ this.handleInputValue } />
</div>
);
}
~ 省略 ~

次にForm.jsを修正します。


./react_sample/frontend/src/Form.js

import React, { Component } from 'react';

class Form extends Component {
constructor(props){
super(props);
}

render() {
// New.js, Edit.jsのpropsをセットします。
const { title, content, submitValue } = this.props;
return (
// this.props.xxxx という記述で親コンポーネントの関数実行します。
<div onSubmit={ this.props.parentSubmit }>
<form>
<label>title:</label>
<input type="text" name="title" value={ title } onChange={ this.props.parentInputValue } />
<label>content:</label>
<textarea name="content" value={ content } onChange={ this.props.parentInputValue } />
<input type="submit" value={ submitValue } />
</form>
</div>
);
}
}

export default Form;


挙動を確認してみます。

共通化の前と挙動が変わらなければ成功です。

次は、メッセージの削除/絞り込みを実装してみたいと思います。