ゴール
新しく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
起動できたかチェック

webpackerでbootstrapを入れてみよう!
これまでのRailsはapp/assets/stylesheetsとかにCSSを入れてきましたが、webpackでJavascript LibraryとCSSを管理することができます。
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一覧画面をつくりまーす

いつも通り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がやってるのはデベロッパーツールで見るとこういうこと

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')
ドーン!

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

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を更新しています。

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