Rails×ReactでRailsのscaffoldで生成されるような簡単なcrudができるアプリケーションを実装したので、備忘録として残しておこうと思います。
###アプリケーションの概要
タイトルと本文のあるメッセージをCRUD+絞り込み検索するアプリケーション
やったこと
(1) 環境の構築
(2) メッセージ一覧を表示する。
(3) 新規メッセージを作成する。
(4) メッセージ詳細の表示、編集、更新をする。
(5) メッセージを削除する、メッセージを検索する。 (本エントリ)
###やってないこと
- Redux、MobXのステート管理のためのフレームワークの導入
- テストの実装
- CSSの実装
- セキュリティやデータの整合性を確認する実装
- 用語や実装の解説
用語や実装の解説については、参考にさせていただいたサイトのリンクを各実装毎に貼っておりますので、そちらをご確認下さい。
参考サイトの作成者の皆様ありがとうございます。
【ご一読いただくにあたって】
・間違いがありましたらコメントにて教えて下さい。(まだまだ勉強中です。)
##メッセージの削除
削除するフローは以下のようなものをイメージしています。
- [destroy] をクリックするとconfirmメッセージが出てくる。
- OKをクリックすると、メッセージが削除され、一覧が表示される。
- キャンセルをクリックすると削除されずに、一覧が表示される。
###React
List.jsを修正
List.js
を以下のように修正します。
~ 省略 ~
const REQUEST_URL_DELETE = 'http://localhost:3030/messages/'
class List extends Component {
~ 省略 ~
hundleDestroy = (event, message_id) => {
event.preventDefault();
// stateのmessagesを取得する
const messages = this.state.messages;
// 渡ってきたmessage_idとmessage.idが等しいメッセージをspliceする
const removeMessage = (msg, i) => {
if (msg.id==message_id) messages.splice(i,1);
}
messages.some(removeMessage);
// 削除されなかったmessagesをsetStateする
this.setState({
messages: messages
})
fetch(REQUEST_URL_DELETE + message_id, {
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
})
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
this.props.history.push('/');
})
.catch((err) => {
console.error(err);
});
}
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>
<td><Link to={ '/message/' + message.id + '/edit' }>Edit</Link></td>
// javascript:void(0)で遷移を無効化する
// react-confirm というモジュールもあるが今回は window.confirm でお手軽に対応する
<td><a href="javascript:void(0)" onClick={ () => { if (window.confirm('本当に削除しますか?')) this.hundleDestroy(event, message.id) } }>destroy</a></td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
}
~ 省略 ~
Rails(APIサーバ)側でのデータの削除
それでは、次にRails(APIサーバ)を修正してidが渡ってきたときに、該当するメッセージを削除するようにします。
今回の記事のメインはReact側なので、Rails側の解説は省略したいと思います。
class MessagesController < ApplicationController
before_action :set_message, only: [:show, :update, :destroy]
~ 省略 ~
def destroy
@message.destroy!
end
~ 省略 ~
end
Rails.application.routes.draw do
resources :messages, only: [:index, :create, :show, :update, :destroy], format: 'json'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
それでは挙動を確認します。
イメージ上ではconfirmのメッセージが表示されていませんが、ブラウザ上では表示されているかと思います。
##メッセージの検索
メッセージの検索は以下の様なフローをイメージしています。
- titleを検索する。
- 検索窓があってそこに検索ワードを入力するとヒットしたtitleをもつメッセージのみが一覧で表示される。
###React
List.jsを修正
List.js
に検索窓を実装します。
コンポーネントを切っても良いかもしれませんが、今回は切らずに実装したいと思います。
~ 省略 ~
const REQUEST_URL_SEARCH = 'http://localhost:3030/messages/search'
class List extends Component {
constructor(props) {
super(props)
this.state = {
messages: [],
// searchTitleというstateを追加する
searchTitle: '',
};
}
handleInputSearchTitle = (event) => {
event.preventDefault();
const searchTitle = event.target.value;
// 検索ワードをRails側にPOSTで飛ばす
fetch(REQUEST_URL_SEARCH, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: { search: searchTitle }
})
})
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
})
.then((response) => response.json())
.then((responseData) => {
// 検索でヒットしたデータをmessagesにセットする
this.setState({
messages: responseData
})
})
.catch((err) => {
console.error(err);
});
// 検索窓に検索ワードをセットする
this.setState({
searchTitle: searchTitle
});
}
render(){
const { messages } = this.state;
return(
<div>
<div>
<label>SearchTitle:</label>
<input type="text" name="searchTitle" value={ this.state.searchTitle } onChange={ this.handleInputSearchTitle } />
</div>
<table>
~ 省略 ~
</table>
</div>
);
}
}
export default List;
Rails
メッセージの検索(APIサーバ)
今回の記事のメインはReact側なので、Rails側の解説は省略したいと思います。
Rails.application.routes.draw do
resources :messages, only: [:index, :create, :show, :update, :destroy], format: 'json' do
post 'search' => "messages#search", on: :collection, format: 'json'
end
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
class MessagesController < ApplicationController
~ 省略 ~
def search
messages = Message.search_by_title(search_params[:search])
render json: messages
end
private
~ 省略 ~
def search_params
params.require(:message).permit(:search)
end
end
class Message < ApplicationRecord
# validates を追加するのを忘れていたので今更だけど追加する
validates :title, presence: true
def self.search_by_title(key)
where("title LIKE :title", {title: "#{key}%"})
end
end
Rails側の実装を完了したら、localhost:4000
を確認してみます。
以下のような挙動になっていれば成功です。
Titleのバリデーション
このままだとTitleが空の状態で[submit]をクリックした時に、エラーも何も表示されません。
scaffoldだとこんな感じのエラーが出ます。
これに似せてエラーを表示させることもできるのですが、今回はReactっぽく title が空の場合は[submit]がクリックできないという実装をしてみたいと思います。
import React, { Component } from 'react';
class Form extends Component {
constructor(props){
super(props);
// Form.jsにclickableというstateを追加する 初期値は'disabled'
this.state = {
clickable: 'disabled'
};
}
// Form.jsの内のhandleInputValueで、親のparentInputValueとsetStateの2つの処理を実行する
handleInputValue = (event) => {
this.props.parentInputValue(event);
// event.target.value(=title)が空かどうか判定し、'disabled' or false をセットする
this.setState({
clickable: event.target.value.trim() == '' ? 'disabled' : false
});
}
render() {
const { title, content, submitValue } = this.props;
const { clickable } = this.state;
return (
<div>
<form onSubmit={ this.props.parentSubmit }>
<label>title:</label>
// parentInputValueではなく、From.jsのhandleInputValueを一枚かませる
<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={ submitValue } disabled={ clickable } />
</form>
</div>
);
}
}
export default Form;
localhost:4000
を確認します。
少し見にくいですが、[submit]がデフォルトではdisabled
になっていて、文字を入力するとabled
になっていてることが確認できるかと思います。trim()
をしているので、全角・半角スペースのみの場合もdisabled
のままなのが確認できるかと思います。
検索結果がなかった場合の表示
このままだと検索結果がなかった時に、何もメッセージが表示されないので、検索結果がなかった場合は「該当するメッセージはありません。」というメッセージを表示したいと思います。
新しくTableコンポーネントを作って、messages
の数で表示/非表示を分岐します。
さらにmessages
の数が0
の場合にのみ、「該当するメッセージはありません。」を表示します。
以下のように修正します。
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
// Tableコンポーネントを新規でインポートする
import Table from './Table';
~ 省略 ~
class List extends Component {
~ 省略 ~
render(){
const { messages, searchTitle } = this.state;
return(
<div>
<div>
<label>SearchTitle:</label>
<input type="text" name="searchTitle" value={ searchTitle } onChange={ this.handleInputSearchTitle } />
</div>
// searchTitleが存在していて、messages.length==0 だったら「該当するメッセージはありません。」のメッセージ
{ searchTitle && messages.length == 0 && <div>該当するメッセージはありません。</div> }
// componentWillMountの処理が終わるまで「Loading...」のメッセージ
// 通信が完了するまでに初期値で一瞬描画されてしまうらしい...解決策をご存知の方コメントください。
{ messages.length == 0 && <div>Loading...</div> }
{ messages.length > 0 && <Table messages={ messages } parentHundleDestroy={ this.hundleDestroy } /> }
</div>
);
}
}
export default List;
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
class Table extends Component {
constructor(props) {
super(props)
}
render(){
const { messages, parentHundleDestroy } = this.props;
return(
<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>
<td><Link to={ '/message/' + message.id + '/edit' }>Edit</Link></td>
<td><a href="javascript:void(0)" onClick={ () => { if (window.confirm('本当に削除しますか?')) parentHundleDestroy(event, message.id) } }>destroy</a></td>
</tr>
);
}) }
</tbody>
</table>
);
}
}
export default Table;
以上で、全ての実装が完了しました。
ご覧いただきありがとうございます。
基本的な箇所しか実装していないので、以下の項目も実装した方がベターかと思います。
- 引数(props)の入力チェックをする
- ディレクリの整理やファイルの命名をちゃんとする
-
New.js
等は何の new なのかファイルが増えてくると分からなくなる可能性が高いので、ディレクトリを切るか、ファイル名を変更した方が良いと思います。
-
- リファクタリング
-
REQUEST_URL
を各ファイルに記述していますが、configファイル?的なものにまとめた方が良いと思います。
-
他にも「こうした方が良い」というものはあるかもしれませんが、そちらについてはコメントいただけばと思います。
次のチャレンジとして、このアプリケーションをReduxを用いて実装したらどうなるかを実装予定です。
完了したらまたQiitaに掲載しようと思います。