Rails5.2+React+WebpackerでモダンなWebフロントエンド開発を1時間でキャッチアップしよう

目次

ゴール

新しくWebサービスを立ち上げる時に、2018年的なモダンなフロントエンド開発環境をサクッと作れるようになる。今回はRailsメインで、Reactはコンポーネントとして都度使うようなユースケースとして使います。

React in Rails的な感じで、SPAの話ではありません。

兎にも角にもrails newする

rails new rails5.2-react-webpacker -S -T --skip-turbolinks --database=mysql --webpack=react
  • -S Sproketsをスキップ
  • -T Minitestをスキップ
  • –skip-turbolinks turbolinkをスキップ
  • –database=mysql とりあえずmysqlを使います
  • –webpack=react webpacker+reactを使います

formanでサーバー起動設定する

rails sしてbin/webpacker-dev-server起動するのは大変面倒くさいため、foremanを入れます。

Gemfile


group :development do
gem 'foreman'
end
bundle install

foremanはProcfileを準備する必要があるので作ります

Path: /rails5.2-react-webpacker/Procfile

rails: rails s -p 3000
webpack: bin/webpack-dev-server

bundle exec foreman startでrails/webpackが起動する設定ができました


~/workspace/rails5.2-react-webpacker $ bundle exec foreman start
12:38:49 rails.1   | started with pid 68729
12:38:49 webpack.1 | started with pid 68730
12:38:51 rails.1   | => Booting Puma
12:38:51 rails.1   | => Rails 5.1.6 application starting in development
12:38:51 rails.1   | => Run `rails server -h` for more startup options
12:38:51 webpack.1  10% building modules 2/2 modules 0 active                                      . 12:38:51 webpack.1 | Project is running at http://localhost:3035/
12:38:51 webpack.1 | webpack output is served from /packs/
12:38:51 webpack.1 | Content not from webpack is served from /Users/kitahashiryoichi/workspace/rails5.2-react-webpacker/public/packs
12:38:51 webpack.1 | 404s will fallback to /index.html

起動できたかチェック

http://localhost:3000にアクセス
f:id:kitahashi-ryoichi:20180426124117p:plain

webpackerでbootstrapを入れてみよう!

これまでのRailsはapp/assets/stylesheetsとかにCSSを入れてきましたが、webpackでJavascript LibraryとCSSを管理することができます。

Webpack · Bootstrap

Rails5.1からnpmではなくyarnがデフォルトになりました。npmの上位互換なので特に気にせず使えます。

GitHub – yarnpkg/yarn: 📦🐈 Fast, reliable, and secure dependency management.

yarn add bootstrap
yarn add jquery
yarn add popper.js

Bootstrap V4.1はjqueryへの依存とpopper.jsへの依存があるので、合わせてインストールしています。この辺はチュートリアル通り。

Bootstrap.jsの都合上グローバルにjqueryとpopper.jsが居ないと動かない仕様なので、webpackにプラグインとして入れます。 

app/config/webpack/environment.js


const { environment } = require('@rails/webpacker')
const webpack = require('webpack')
environment.plugins.prepend(
'Provide',
new webpack.ProvidePlugin({
$: 'jquery/dist/jquery',
jQuery: 'jquery/dist/jquery',
Popper: 'popper.js/dist/popper'
})
)
module.exports = environment

CSSとJSの読み込み先をwebpackにする

app/views/layouts/application.html.erb

【before】

<%= stylesheet_include_tag 'application' %>
<%= javascript_include_tag 'application' %>

【after】

<%= stylesheet_pack_tag 'application' %>
<%= javascript_pack_tag 'application' %>

これでRails+WebpackerでBootstrapまで使えるようになりましたー!

webpackerでReactが動く所まで設定する

このままだと諸々Reactは動かないのでいくつかライブラリを入れます。

yarn add webpacker-react

各種ライブラリーとbabel-polyfillをimportします。

app/javascript/packs/application.js


import 'babel-polyfill'
import 'stylesheets/application'
import 'bootstrap'
import WebpackerReact from 'webpacker-react'
console.log('Hello World from Webpacker')

railsのgenerators設定もついでにしとこう

webpackを使うのでassetsにjs/cssを吐かないように、helperも切っときます。

app/config/application.rb

config.generators do |g|
g.assets false
g.helper false
end

さっそくCRUD画面作ってみよー

/todos TODO一覧画面をつくりまーす

f:id:kitahashi-ryoichi:20180426134513p:plain

いつも通りtodos_controllerを作って


rails g controller todos

config/routes.rb

Rails.application.routes.draw do
resources :todos
end

app/controllers/todos_controller.rb

class TodosController < ApplicationController
def index
@todos = %w(hoge hage)
end
end

viewでreact-railsのhelperメソッドreact_componentを使います

app/views/todos/index.html.erb

<%= react_component('Todos', todos: @todos) %>

react_componentがやってるのはデベロッパーツールで見るとこういうこと

f:id:kitahashi-ryoichi:20180426140043p:plain

app/javascript/packs/todos.jsx

import React from 'react'
export default class Todos extends React.Component {
    render() {
        const { todos } = this.props
        return (
            <ul className="list-group">
                {todos.map(todo => <li key={todo} className="list-group-item">{todo}</li>)}
            </ul>
        )
    }
}

app/javascript/packs/application.js

import "babel-polyfill"
import 'stylesheets/application'
import 'bootstrap'
import Todos from './todos'
import WebpackerReact from 'webpacker-react'
WebpackerReact.setup({
    Todos
})
console.log('Hello World from Webpacker')
http://localhost:3000/todosにアクセスすると

ドーン! 

f:id:kitahashi-ryoichi:20180426135526p:plain

TODO追加機能を作りまーす

今回はモデルを変更するのは割愛して、画面上だけで追加することにしました。

f:id:kitahashi-ryoichi:20180426161345p:plain

app/javascript/packs/todos.jsx

import React from 'react'
export default class Todos extends React.Component {
    constructor(props) {
        super(props)
        this.state = { todos: props.todos }
    }
    addTodo() {
        const { todos } = this.state
        todos.push('追加したよ')
        this.setState({ todos })
    }
    render() {
        const { todos } = this.state
        return (
            <React.Fragment>
                <ul className="list-group">
                    {todos.map(todo => <li key={todo} className="list-group-item">{todo}</li>)}
                </ul>
                <button type="button" className="btn btn-primary" onClick={() => this.addTodo()}>
                    TODO追加
</button>
            </React.Fragment>
        )
    }
}

propsをTodosコンポーネントのstateで管理するようにして、addTodoメソッドを生やしました。簡単!

onClickの書き方は3種類ありますが、今回はアロー演算子で書いています。 

1. onClick = { event => this.addTodo(event) }
2. onClick = { this.addTodo.bind(this) }
3. constructorでバインドしておく
constructor(props) {
    super(props)
    this.addTodo = this.addTodo.bind(this)
}
onClick = { this.addTodo }

Todoを変更できるようにする

Todosは一覧を管理するコンポーネントでTodoは個別のTodoを管理するコンポーネントとして切り出して実装することにします。

app/javascript/packs/todos.jsx

import React from 'react'
import Todo from './components/todo'
export default class Todos extends React.Component {
    constructor(props) {
        super(props)
        this.state = { todos: props.todos }
    }
    addTodo() {
        const { todos } = this.state
        todos.push('追加したよ')
        this.setState({ todos })
    }
    handleUpdateTodo(todo, i) {
        const { todos } = this.state
        todos[i] = todo
        this.setState({ todos })
    }
    render() {
        const { todos } = this.state
        return (
            <React.Fragment>
                <ul className="list-group">
                    {todos.map((todo, i) =>
                        <Todo
                            key={todo}
                            todo={todo}
                            handleUpdateTodo={todo => this.handleUpdateTodo(todo, i)}
                        />
                    )}
                </ul>
                <button type="button" className="btn btn-primary" onClick={() => this.addTodo()}>
                    TODO追加
</button>
            </React.Fragment>
        )
    }
}

app/javascript/packs/components/todo.jsx

import React from 'react'
export default class Todo extends React.PureComponent {
    constructor(props) {
        super(props)
        this.state = { todo: props.todo, isEdit: false }
    }
    editMode() {
        this.setState({ isEdit: true })
    }
    updateTodo(todo) {
        this.setState({ todo })
    }
    save() {
        const { handleUpdateTodo } = this.props
        const { todo } = this.state
        this.setState({ isEdit: false })
        handleUpdateTodo(todo)
    }
    render() {
        const { todo, isEdit } = this.state
        return (
            <li
                className="list-group-item"
                onClick={() => this.editMode()}
            >
                {isEdit
                    ?
                    <div className="input-group">
                        <input
                            type="text"
                            defaultValue={todo}
                            onChange={e => this.updateTodo(e.target.value)}
                            className="form-control"
                            autoFocus
                        />
                        <div className="input-group-append">
                            <button
                                type="button"
                                className="btn btn-outline-secondary"
                                onClick={() => this.save()}
                            >
                                保存
</button>
                        </div>
                    </div>
                    : todo}
            </li>
        )
    }
}

こんな具合で、TodoのonChangeをトリガーにTodoのstateを更新して、保存のタイミングでTodosから受け取ったhandleUpdateTodoでTodosのstateを更新しています。

f:id:kitahashi-ryoichi:20180426165032p:plain

今回のソースコードはコチラ

GitHub – kitahashiryoichi/rails5.2-react-webpacker: Rails5.2+React+Webpacker modern frontend tutorial

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
目次
閉じる