Ruby
Rails
reactjs

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

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

アプリケーションの概要

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

やったこと

(1) 環境の構築
(2) メッセージ一覧を表示する。
(3) 新規メッセージを作成する。
(4) メッセージ詳細の表示、編集、更新をする。(本エントリ)
(5) メッセージを削除する、メッセージを検索する。

やってないこと

  • Redux、MobXのステート管理のためのフレームワークの導入
  • テストの実装
  • CSSの実装
  • セキュリティやデータの整合性を確認する実装
  • 用語や実装の解説

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

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

事前準備

一覧表示をscaffoldに寄せる

Railsのscaffoldで生成される一覧の表示は以下のようなものです。
スクリーンショット 2018-02-11 22.34.28.png

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;

そうすると表示は以下のようになったと思います。
若干違いますが、これで進めていきます。
スクリーンショット 2018-02-11 22.50.46.png

メッセージの詳細

詳細画面まわりのフローは以下のような流れをイメージしています。
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 のデータが返ってくることを確認します。
スクリーンショット 2018-02-11 23.38.32.png

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;

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

AwesomeScreenshot-2018-02-11T15-04-43-781Z.gif
(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を確認してみます。
以下のような挙動になっていれば成功です。
AwesomeScreenshot-2018-02-13T13-53-25-814Z.gif

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;

挙動を確認してみます。
共通化の前と挙動が変わらなければ成功です。
次は、メッセージの削除/絞り込みを実装してみたいと思います。