Ruby on Rails+ReactでCRUDを実装してみた

Ruby on Rails+ReactでCRUDを実装してみた

ReactとRailsの勉強を兼ねて、CRUDを実装してみました!Viewはテキトーです。

(React歴2ヶ月目なのでコードの品質は大目にみてください。)

## 読者の対象レベル

Reactのコンポーネントを理解している人
RailsでCRUDを実装できるくらいの人

## 実行環境

Reduxを使わずReactだけで実装しています。

バックエンド　Ruby on Rails  5.1.5
フロントエンド React 16.2.0

RailsでReactを使う方法はいくつかありますが、今回はwebpack + babel-loaderを利用してバックエンドとフロントエンドを切り離して作成し巻いた。RailsはAPIだけです。

## デモ

[![https://gyazo.com/0b9aaa8df0895a451de8a9d4d04a0bcd](https://i.gyazo.com/0b9aaa8df0895a451de8a9d4d04a0bcd.gif)](https://gyazo.com/0b9aaa8df0895a451de8a9d4d04a0bcd)

## ディレクトリ構成

ディレクトリ構成はこのような感じです。webpackの環境は`crud_front`にあります。

```
.
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app
├── bin
├── config
├── config.ru
├── crud_front
├── db
├── lib
├── log
├── public
├── test
├── tmp
└── vendor
```

ちなみに`crud_front`はこんな感じ。

```
.
├── README.md
├── node_modules
├── package-lock.json
├── package.json
├── public
├── src
└── yarn.lock
```

## RailsでAPIを作る

```sh
rails new app -d mysql --api
```

gemfileのrack-corsのコメントアウトを外し、

```sh
bundle install 
```

次にapp階層に移動し、モデルの作成を行います。

```sh
rails g model product product:text

rake db:create              
rails db:migrate
```

何もないと寂しいので`db/seeds.rb`にseedデータを作っていきます。

```ruby
Product.create([
  {product: "犬"},
  {product: "猫"},
  {product: "馬"}
])

```

productは何でもいいので、実行します。

```sh
rake db:seed   
```

### コントローラーの作成

今回はAPIモードなのでViewファイルは作成されません。

```sh
rails g controller products
```

`app/controllers/products_controller.rb`の中身はこんな感じ

```ruby
class ProductsController < ApplicationController

  def index
    @product = Product.all
    render json: @product
  end

  def create
    @product = Product.create(product: params[:product])
    render json: @product
  end

  def update
    @product = Product.find(params[:id])
    @product.update_attributes(product: params[:product])
    render json: @product
  end

  def destroy
    @product = Product.find(params[:id])
    if @product.destroy
      head :no_content, status: :ok
    else
      render json: @product.errors, status: :unprocessable_entity
    end
  end

end
```

ついでに`config/routes`ルーティングの設定も行います。

```ruby
Rails.application.routes.draw do
  resources :products
end
```

## Railsを起動させてJsonで返ってきてるか確認

React側で3000番ポートを使用するので、RailsのAPIサーバーは3001番ポートに設定します。

```sh
rails s -p 3001
```

ではGetのエンドポイントにアクセスします。

```console
$ curl -G http://localhost:3001/products/

#出力
[{"id":1,"product":"犬","created_at":"2018-02-16T11:49:35.000Z","updated_at":"2018-02-16T11:49:35.000Z"},{"id":2,"product":"猫","created_at":"2018-02-16T11:49:35.000Z","updated_at":"2018-02-16T11:49:35.000Z"},{"id":3,"product":"馬","created_at":"2018-02-16T11:49:35.000Z","updated_at":"2018-02-16T11:49:35.000Z"}]%   
```

ちゃんとJsonで返ってきました。ですがRails側で3000番ポートのアクセスが許可されていないので`application.rb`に以下を記述しましょう。

```ruby

class Application < Rails::Application
    config.load_defaults 5.1

    config.api_only = true
    config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins 'http://localhost:3000'
        resource '*',
        :headers => :any,
        :methods => [:get, :post, :patch, :delete, :options]
      end
    end
  end
```

## Reactの環境開発

めんどくさいReactの環境開発を構築してくる便利なツール「[create-react-app](https://www.npmjs.com/package/create-react-app-module)」があるのでそちらを使用していきます。

```sh
npm install -g create-react-app
```

これで「create-react-app」が使用できるようになったので、`crud_front`という名でプロジェクトを作成していきます。

```sh
create-react-app crud_front
```

では`crud_front`の階層に移動しサーバーを起動してみます。

```sh
cd crud_front
npm start
```

 http://localhost:3000 にアクセスすると以下のような画面が出てきます。

<img width="1408" alt="スクリーンショット 2018-02-17 11.35.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/179816/33b03dd4-8ca8-b08d-1e94-dc99596f493d.png">



ページのコンテンツは、`src/App.js`ファイルのReactコンポーネントを通してレンダリングします。

```jsx
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

```

## Reactコンポーネントを使ったデータの表示

コンポーネント構成はこのような感じにしてみました。

<img width="773" alt="スクリーンショット 2018-02-17 11.39.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/179816/cac9c7d7-0a7a-7050-8a0d-35d9e974077e.png">

メインコンポーネントの上につぶやきフォームとつぶやきリスト。つぶやきリストの上に消去ボタンと編集フォームという構成です。

親と子の関係を表すとこんな感じ

<img width="744" alt="スクリーンショット 2018-02-17 11.48.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/179816/62829f3c-ea5f-e396-c8a0-729c10ef05e4.png">


`crud_front/src/`にComponentsディレクトリを作り、Reactをここに記述していきます。最終的なディレクトリ構成はこのようになります。

```
.
├── App.css
├── App.js
├── App.test.js
├── Components
│   ├── FormContainer.jsx
│   ├── MainContainer.jsx
│   ├── ProductsContainer.jsx
│   └── ViewProduct.jsx
├── index.css
├── index.js
├── logo.svg
└── registerServiceWorker.js
```

では`src/App.js`に以下を記述していきましょう。

```jsx
import React, { Component } from 'react';
import MainContainer from './Components/MainContainer'
import './App.css';



class App extends Component {
  render() {
    return (
      <div className="App">
        <MainContainer/>
      </div>
    );
  }
}

export default App;
```

このMainContainerにRailsと通信するAjax部分を記述していきます。今回はAPIと通信する便利なライブラリ[axios](https://www.npmjs.com/package/axios)を使用していきます。あと[bootstrap](https://react-bootstrap.github.io/)と[react-addons-update](https://www.npmjs.com/package/react-addons-update)をインストールしていきましょう。

```sh
yarn add axios  
yarn add react-addons-update
yarn add react-bootstrap
```

そしてbootstrapもインストールしていきます。

```sh
npm i bootstrap@3 --save
```

`src/Components`ディレクトリにMainContainerを作成し以下を記述していきます。ここでaxiosを使いRailsAPIと通信を行い、結果はresultsに格納されます。ではrailsとwebpackを起動させみてみましょう。

```jsx
import React from 'react'

class MainContainer extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      products: []
    }
  }


componentDidMount() {
    axios.get('http://localhost:3001/products')
    .then((results) => {
      console.log(results)
      this.setState({products: results.data})
    })
    .catch((data) =>{
      console.log(data)
    })
  }

  render() {
    return (
      <div className='app-main'>
      </div>
    );
  }
}

export default MainContainer
```

```sh
rails s -p 3001
npm start
```

もし`npm start`でwebpackが起動しなければ一度`npm install`を行なってみてください。

おそらく何も表示されませんが、ブラウザのコンソールを開いてみるとconsole.log(results)の結果が表示されているはずです。

<img width="1039" alt="スクリーンショット 2018-02-17 12.23.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/179816/7a63b1a8-0225-1bad-b158-349ac5d897ef.png">

この中のdataにデータベースのデータが格納されています。result.dataとするとデータを取得することができますので、これをproductsに保存しています。

では`src/components/ProductsContainer.jsx`と`ViewProduct.jsx`にデータを表示させるコンポーネントを作成していきます。

まずは`ProductsContainer.jsx`に以下を記述しましょう。

```jsx
import React from 'react'
import ViewProduct from './ViewProduct'

class ProductsContainer extends React.Component {
  render() {
    return(
      <div className='productList'>
         {this.props.productData.map((data) => {
           return(
               <ViewProduct data={ data } key={ data.id } />
           )
         })}
      </div>
    )
  }
}

export default ProductsContainer
```

次に`ViewProduct.jsx`に以下を記述します。

```jsx
import React from 'react'

class ViewProduct extends React.Component {

  render() {
    return(
      <div>
        <span>{ this.props.data.product }</span>
      </div>
    )
  }
}

export default ViewProduct
```

最後に親のコンポーネントである`MainContainer.jsx`に`ProductsContainer`を付け加えます。

```jsx
import ProductsContainer from './ProductsContainer'

class MainContainer extends React.Component {
  // 略
  render() {
    return (
      <div className='app-main'>
        <ProductsContainer productData={ this.state.products } />
      </div>
    );
  }
}

```

ブラウザを見てみるとこんな感じに表示されていると思います。

<img width="1236" alt="スクリーンショット 2018-02-17 12.43.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/179816/5f27672b-0dd1-3891-e7ba-cb2d2e76196c.png">


## Product作成フォームの作成

`src/Components/FormContainer.jsx`を作成し以下を記述していきます。

```jsx
import React from 'react'
import {Button,FormGroup,FormControl} from 'react-bootstrap'

class FormContainer extends React.Component {
  render(){
    return(
      <div>
      <form>
        <FormGroup controlId="formBasicText">
          <FormControl
            type="text"
            value={this.state.product}
            placeholder="Enter text"
            onChange={ e => this.onChangetext(e)}
          />
        </FormGroup>

      </form>
      <Button type="submit" onClick={this.hundleSubmit}>つぶやく</Button>
      </div>
    )
  }
}

export default FormContainer
```

次に`MainContainer.jsx`にFormContainerを記述しましょう。

```jsx
import FormContainer from './FormContainer'

class MainContainer extends React.Component {
  // 略
  render() {
    return (
      <div className='app-main'>
        <FormContainer />
        <ProductsContainer productData={ this.state.products } />
      </div>
    );
  }
}
```

おそらくブラウザをみるとこんな感じで表示されているはずです。

<img width="1241" alt="スクリーンショット 2018-02-17 12.52.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/179816/f6e7f24d-0f97-464b-47db-90c968566600.png">

まだフォームの中に文字を打っても何も変更できないはずなので、変更可能になるようにコードを書いていきます。
`FormContainer.jsx`に以下を記述しましょう。

```jsx
import React from 'react'
import {Button,FormGroup,FormControl} from 'react-bootstrap'

class FormContainer extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      product: ''
    }
  }

  onChangetext(e) {
    this.setState({product: e.target.value})
  }


  render(){
    return(
      <div>
      <form>
        <FormGroup controlId="formBasicText">
          <FormControl
            type="text"
            value={this.state.product}
            placeholder="Enter text"
            onChange={ e => this.onChangetext(e)}
          />
        </FormGroup>

      </form>
      <Button type="submit" onClick={this.hundleSubmit}>つぶやく</Button>
      </div>
    )
  }
}

export default FormContainer
```

これでフォームを変更することができるようになったはずです。最後にbootstrapを適用させるために`MainContainer.jsx`にコードを書いていきます。

```javascript
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap/dist/css/bootstrap-theme.css';
```

<img width="634" alt="スクリーンショット 2018-02-17 13.04.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/179816/373923b0-cabe-030e-1082-32f50ff1d711.png">

## フォーム内容を適用させる

つぶやきフォームコンポーネントで入力したデータをつぶやきリストコンポーネントに表示させたいので、親コンポーネントであるMainContainerを経由してデータを反映させていきます。

<img width="832" alt="スクリーンショット 2018-02-17 13.09.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/179816/b20f7586-2cf5-37e8-9903-ae71a5169473.png">

その関係上RailsAPIと通信するAjaxは`MainContainer.jsx`に記述していきます。

```jsx
import update from 'react-addons-update' ←ここも追加

class MainContainer extends React.Component {
  //略
  createProduct = (product) => {
    axios.post('http://localhost:3001/products',{product: product} )
    .then((response) => {
      const newData = update(this.state.products, {$push:[response.data]})
      this.setState({products: newData})
    })
    .catch((data) =>{
      console.log(data)
    })
  }

  render() {
    <div className='app-main'>
        <FormContainer createProduct={this.createProduct}/>
        <ProductsContainer productData={ this.state.products } />
    </div>
  }
}
```

この`createProduct`関数をつぶやきフォームコンポーネントに渡してやり実行させます。その結果をメインコンポーネントで保存し、そのデータをつぶやきリストに渡します。

では`FormContainer.jsx`を編集していきます。

```javascript

class FormContainer extends React.Component {
  //略
  hundleSubmit = () => {
    this.props.createProduct(this.state.product)
    this.setState({product:''})
  }

  render(){
    return(
    //略
    )
  }
}
```

これで以下のようなことができると思います。

[![https://gyazo.com/8704b21f9b4b02b09daa0daf515433d6](https://i.gyazo.com/8704b21f9b4b02b09daa0daf515433d6.gif)](https://gyazo.com/8704b21f9b4b02b09daa0daf515433d6)

## データの削除と更新

次にデータの削除と更新を書いていこうと思います。削除と更新も作成の時と同じようにメインコンポーネントでRailsAPIと通信する関数を定義して消去コンポーネントと更新コンポーネントをに渡して実行します。

`src/components/MainContainer.jsx`に以下のコードを記述します。

```jsx
class MainContainer extends React.Component {
  //略
  deleateProduct = (id) => {
    axios.delete(`http://localhost:3001/products/${id}`)
    .then((response) => {
      const productIndex = this.state.products.findIndex(x => x.id === id)
      const deletedProducts = update(this.state.products, {$splice: [[productIndex, 1]]})
      this.setState({products: deletedProducts})
      console.log('set')
    })
    .catch((data) =>{
      console.log(data)
    })
  }

  updateProduct = (id, product) => {
    axios.patch(`http://localhost:3001/products/${id}`,{product: product})
    .then((response) => {
      const productIndex = this.state.products.findIndex(x => x.id === id)
      const products = update(this.state.products, {[productIndex]: {$set: response.data}})
      this.setState({products: products})
    })
    .catch((data) =>{
      console.log(data)
    })
  }

  render() {
    return (
      <div className='app-main'>
        <FormContainer  hendleAdd={this.hendleAdd} createProduct={this.createProduct}/>
        <ProductsContainer productData={this.state.products} deleateProduct={this.deleateProduct} updateProduct={this.updateProduct}/>
      </div>
    );
  }
}
```

`src/Components/ProductsContainer.jsx`にも以下のように記述します。

```jsx
class ProductsContainer extends React.Component {
  render() {
    return(
      <div className='productList'>
         {this.props.productData.map((data) => {
           return(
               <ViewProduct data={data} key={data.id} onDelete={this.props.deleateProduct} onUpdate={this.props.updateProduct}/>
           )
         })}
      </div>
    )
  }
}
```

次に`src/Components/ViewProduct.jsx`以下のように記述します。

```jsx
import React from 'react'
import {Button} from 'react-bootstrap'

class ViewProduct extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      updateText: '',
    }
  }


  handleDeleate = () => {
    this.props.onDelete(this.props.data.id)
  }

  handleUpdate = () => {
    this.props.onUpdate(this.props.data.id, this.state.updateText)
  }

  handleInput = (e) => {
    this.setState({updateText: e.target.value})
  }


  render() {
    return(
      <div>
        <span>{this.props.data.text}</span>
        <span className='deleteButton' onClick={this.handleDeleate}>X</span>
        <span>
          <input type="text" value={this.state.updateText} onChange={e => this.handleInput(e)} />
        </span>
        <span>
          <Button type="submit" onClick={this.handleUpdate}>更新！</Button>
        </span>
      </div>
    )
  }
}

export default ViewProduct
```

最後にデモと同じ見た目になるように`App.css`を編集していきます。

```css
.app-main{
  width:auto;
  height: auto;
  display: flex;
  justify-content: space-around;
}

.deleteButton {
  padding: 10px;
  font-size: 20px;
  color: red;
}
```

これで完成です！！

<img width="898" alt="スクリーンショット 2018-02-17 13.42.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/179816/3f829015-a704-5447-2b28-b40bfd336f29.png">

## まとめ

コンポーネント間のデータのやりとりがめんどくさかったので、次はReduxを勉強していこうと思います。

Github
https://github.com/yoshimoto8/React_CRUD

参照
http://techlife.cookpad.com/entry/2016/07/27/101015
http://allprowebdesigns.com/2017/09/how-to-build-a-react-app-that-works-with-a-rails-5-1-api/