Ruby
Rails
reactjs

Rails × React で Rails の scaffold ライクなアプリケーションを実装 - 新規登録 -

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

アプリケーションの概要

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

やったこと

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

やってないこと

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

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

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

新規メッセージを登録

スクリーンショット 2018-02-09 9.50.14.png

新規登録のフローは以下のような流れをイメージしています。
1. [New Message] をクリックすると新規登録画面が表示される。
2. 新規登録画面では、メッセージのタイトルと詳細を入力するフォームが表示されている。
3. [submit]をクリックするとメッセージが登録されて、一覧が表示される。
4. 一覧画面では 3.で登録したメッセージのタイトルが表示されている。

画面 URL
一覧画面 http://localhost:4000/
新規登録画面 http://localhost:4000/new

React

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

New.jsを作成
./react_sample/frontend/src
touch New.js
./react_sample/frontend/src/New.js
import React, { Component } from 'react';

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

export default New;
ルーティングを設定

ReactはSPAなので、URLを変えずに新規登録画面(Newコンポーネント)を表示することも可能だと思うのですが、今回は新規登録画面(Newコンポーネント)のURLをhttp://localhost:4000/newというように設定したいと思います。

react-routerのインストール
./react_sample/frontend
yarn add react-router react-router-dom
App.jsにルーティングを設定
./react_sample/frontend/App.js
import React, { Component } from 'react';
// react-router-dom をインポートする
import { BrowserRouter as Router, Link, Switch, Route } from 'react-router-dom';
import List from './List';
import New from './New';

const App = () => (
  // <Router>でAppコンポーネントをラップする
  <Router>
    <div>
      <AppHeader />
      // 現在のURLが path と一致した場合に component= でセットしているコンポーネントをレンダリングする
      <Switch>
         // exact によって、path の記述と一致するURLの場合にのみレンダリングする
         // exact がないと /new/example という path があったとしても /new の方がレンダリングされてしまう
         <Route exact path='/' component={ List }/>
         <Route exact path='/new' component={ New } />
       </Switch>
      <AppFooter />
    </div>
  </Router>
)

const AppHeader = () => (
  <div>
    <h3>Message App</h3>
    <p>
      // Link to で a が生成される
      <Link to="/">[Home]</Link>
      <Link to="/new">[New Message]</Link>
    </p>
  </div>
)

const AppFooter = () => (
  <div>
    <p>
      Message App Footer
    </p>
  </div>
)

export default App

[Home]をクリックするとURLが '/' となりListコンポーネントがレンダリングされて、
[New Message]をクリックするとURLが '/new' となり、Newコンポーネントがレンダリングされます。

コンポーネントは以下のようなイメージです。

スクリーンショット 2018-02-14 10.09.04.png

それでは localhost:4000を確認してみます。
AwesomeScreenshot-2018-02-10T11-28-01-174Z.gif

新規登録画面でメッセージのタイトルと詳細を入力するフォームが表示されるようにする

入力フォームを実装

次にNew.jsにメッセージの入力フォームを実装していきます。

./react_sample/frontend/src/New.js
import React, { Component } from 'react';

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

export default New;

を以下のように修正します。

./react_sample/frontend/src/New.js
import React, { Component } from 'react';

class New extends Component {
  // コンポーネントの初期化 インスタンスを作成したタイミングで実行される
  constructor(props){
    // super(props)を呼び出すことで、 this を使用できる
    super(props);
    // Message の title と content を this.state にセットする
    this.state = {
      title: '',
      content: ''
    };
  }

  render() {
   // const にセットすることで、 value={ this.state.title } と書かなくて済む
   const { title, content } = this.state;
   return (
     // return では1つのコンポーネントしか返すことが出来ない
     // return (
     //       <div>...</div>
     //       <div>...</div>
     // );
     // ↑はエラーとなる
     <div>
       <p>New Message</p>
       <div>
         <form>
           <label>title:</label>
           <input type="text" name="title" value={ title } />
           <label>content:</label>
           <textarea name="content" value={ content } />
           <input type="submit" value="Submit" />
         </form>
       </div>
     </div>
   );
 }
}

export default New;

constructorsuper(props) の詳細は、公式リファレンスを確認するといいかもです。

これで入力フォームが表示されたと思いますので、localhost:4000 を確認してみましょう。(イメージは省略)

しかし、今のままだと title に文字を入力しても、文字がinputタグに反映されないと思います。
文字を反映させるには inputタグ や textareaタグ に onChangeイベント を実装し、 setState を更新しなければなりません。

./react_sample/frontend/src/New.js
import React, { Component } from 'react';

class New extends Component {
  constructor(props){
    super(props);
    this.state = {
      title: '',
      content: ''
    };
    // this を bindする
    this.handleInputValue = this.handleInputValue.bind(this);
  }
  // handleInputValue を実装
  handleInputValue(event){
    const field = event.target.name;
    this.state[field] = event.target.value;
    // setState することでレンダリングされる
    this.setState({
      [field]: this.state[field]
    });
  }

  render() {    
   const { title, content } = this.state;
   return (
     <div>
       <p>New Message</p>
       <div>
         <form>
           <label>title:</label>
           // onChange で 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="Submit" />
         </form>
       </div>
     </div>
   );
 }
}

export default New;
アロー関数

handleInputValue のような関数が増えていく度に constructor でbindするのは手間です。
アロー関数を使うことでその手間を削減することができるので、アロー関数を使えるように設定したいと思います。

babel-preset-stage-0 をインストールします。

./react_sample/forntend
yarn add babel-preset-stage-0

.babelrc を以下のように編集します。

./react_sample/forntend/.babelrc
{
  "presets": [
    "es2015", "react", "stage-0" # "stage-0"を追加
  ]
}

これでアロー関数を使えるようになったので、 New.js を以下の様に修正します。

./react_sample/frontend/src/New.js
import React, { Component } from 'react';

class New extends Component {
  constructor(props){
    super(props);
    this.state = {
      title: '',
      content: ''
    };
    // this.handleInputValue = this.handleInputValue.bind(this);
  }

  //handleInputValue(event){
  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;
   return (
     <div>
       <p>New 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="Submit" />
         </form>
       </div>
     </div>
   );
 }
}

export default New;

[submit]をクリックするとメッセージが登録されて、一覧が表示される

それでは次に [submit]かクリックされてメッセージが登録されるまでを実装します。

handleSubmitを実装

submitがクリックされた時のイベントを実装します。

./react_sample/frontend/src/New.js
import React, { Component } from 'react';

// Request を送るURLを指定(RailsのAPIサーバ)
const REQUEST_URL = 'http://localhost:3000/messages.json'

class New extends Component {
  constructor(props){
    super(props);
    this.state = {
      title: '',
      content: ''
    };
  }

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

  // submitがクリックされた時の処理
  handleSubmit = (event) => {
    // デフォルトの遷移を抑制する
    event.preventDefault();
    // $.ajax()に代替する fetch 
    // 結果がPromiseで返される
    fetch(REQUEST_URL, {
      method: 'POST',
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
      //  JSON.stringifyで 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>New Message</p>
       <div>
         <form onSubmit={ this.handleSubmit }>
           <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="Submit" />
         </form>
       </div>
     </div>
   );
 }
}

export default New;

まだRails側を実装していないので、登録は出来ませんが、localhost:4000で挙動を確認します。
AwesomeScreenshot-2018-02-10T13-04-11-559Z.gif

(スクリーンショットの動画機能の精度が悪いせいだと思うのですが、レンダリングが遅く見えますが、実際にはこんなに遅くないと思います。実際遅くないです。)

Rails

メッセージの登録(APIサーバ)

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

./react_sample/app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def index
    messages = Message.all
    render json: messages
  end

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

  private

  def message_params
    params.require(:message).permit(:title, :content)
  end
end
./react_sample/config/routes.rb
Rails.application.routes.draw do
  resources :messages, only: [:index, :create], format: 'json'
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

Rails側の実装を完了したら、localhost:4000を確認してみます。
以下のような挙動になっていれば成功です。

AwesomeScreenshot-2018-02-10T13-28-04-832Z.gif

次は、メッセージの詳細/編集/更新を実装してみたいと思います。こちら