Why I’m using Flux and React in Rails
I've been integrating react into my rails apps for a little while now. You can see the 3 main ways I've been integrating react with rails here, http://www.openmindedinnovations.com/blogs/3-ways-to-integrate-ruby-on-rails-react-flux. The simplest way for a rails dev to get up and running with react is to use the react rails gem. A lot of developers having success with that, but as your app scales you'll find a lot of problems when not using a front end data managment layer like flux or some equivilent. There are lot of other good systems out there including my current favorite which is using webpack/redux/react (https://github.com/shakacode/react_on_rails), but I'm going to focus on integrating flux into rails using the react rails gem. The key to this setup is that I don't use npm at all and I just use sprockets, which makes the process much smoother for a rails developer.
I'll be using the following libraries in this template:
Alt (http://alt.js.org/)
React Rails (https://github.com/reactjs/react-rails)
Lodash (https://lodash.com/)
Some of these are preference, but this tutorial will walk you through the core setup so you can use whatever you want.
Setup React Rails Gem
First you will need to add the react rails gem to your rails project. Go to their github page for more details on setup, https://github.com/reactjs/react-rails. All you need to do is add it to your gem file, bundle install, and run their generator.
rails g react:install
You'll end up with a components.js file in your javascripts that loads all the react code. This file is special because react rails loads this file using exec js if you use their server rendering feature. This is important because if you use flux, you'll need to makes sure all of your flux assets are loaded into this component.js file as well. Execjs will need all of it to properly server render your components. I changed the name of the folder referenced in the components.js file because we're going to be adding flux so we'll have more than just components in our react code.
Now let's start by making a model to play with. We're going to do a simple todo app where you can cross off things on your todo list and add things to your todo list.
rails g resource Todo name:string checked:boolean
I made the root go to the todos index.
# config/routes.rb
Rails.application.routes.draw do
resources :todos
root 'todos#index'
end
Lets's start by adding an index action to our todo controller
# app/controllers/todos_controller.rb
class TodosController < ApplicationController
def index
end
end
Let's add an index file to our todos views and call a TodoIndexPage component using the react_component helper from react rails.
# app/views/todos/index.html.erb
<%= react_component('TodoIndex') %>
Now we need to make that component that we just called. We'll create the component in 'react' folder that react rails created.
# app/assets/javascripts/react/TodoIndex.js.coffee
{ div, h1 } = React.DOM
window.TodoIndex = React.createClass
render: ->
div {},
h1 {}, 'Todo List'
Run your migrations, start your server, and go to localhost:3000. You should see a simple Todo List title at the top.
Pass data from rails to react
Now let's add some data to the todo list. We're going to list out all the todos on the index page and pass them to the react component using jbuilder.
# app/controllers/todos_controller.rb
class TodosController < ApplicationController
def index
@todos = Todo.all
end
end
I like to create an index jbuilder file that uses a partial to keep things modular.
# app/views/todos/index.json.jbuilder
json.todos @todos, partial: 'todos/todo', as: :todo
# app/views/todos/_todo.json.jbuilder
json.extract!(todo, :id, :name, :checked)
Now let's pass it to the component and render it out in the component code.
# app/views/todos/index.html.erb
<%= react_component('TodoIndex', render(template: 'todos/index.json.jbuilder')) %>
I'm going to add lodash so I can loop over the todos passed in.
# app/assets/javascripts/react/TodoIndex.js.coffee
{ div, h1, ul, li } = React.DOM
window.TodoIndex = React.createClass
render: ->
div {},
h1 {}, 'Todo List'
_.map @props.todos, (todo)=>
li {}, todo.name
Now I'm going to quickly add bootstrap and some visuals to make this a little cleaner.
# app/assets/javascripts/react/TodoIndex.js.coffee
{ div, h1, ul, li, a, span } = React.DOM
window.TodoIndex = React.createClass
render: ->
div className: 'container',
div className: 'row',
div className: 'col-xs-12',
h1 {}, 'Todo List'
ul className: 'list-unstyled',
_.map @props.todos, (todo)=>
li className: 'list-item',
a className: 'btn btn-primary', 'Check'
span className: 'list-text', todo.name
# app/assets/stylesheets/todos.scss
.list-item {
margin-bottom: 20px;
.btn {
margin-right: 10px;
}
&.checked {
.btn {
opacity: .7
}
.list-text {
text-decoration: line-through;
}
}
}
Add a form to submit new todos using ajax without Flux
Now let's add a form and break up our list into a TodoList with TodoListItems.
# app/assets/javascripts/react/TodoIndex.js.coffee
{ div, h1, ul, li, a, span, label, input } = React.DOM
TodoForm = React.createFactory React.createClass
render: ->
div className: 'form-group',
label {}, 'Enter Todo'
input className: 'form-control', placeholder: 'Enter todo name'
TodoListItem = React.createFactory React.createClass
render: ->
li className: 'list-item',
a className: 'btn btn-primary', 'Check'
span className: 'list-text', @props.todo.name
TodoList = React.createFactory React.createClass
render: ->
ul className: 'list-unstyled',
_.map @props.todos, (todo)=>
TodoListItem(todo: todo)
window.TodoIndex = React.createClass
getInitialState: ->
todos: []
componentWillMount: ->
@setState(todos: @props.todos)
render: ->
div className: 'container',
div className: 'row',
div className: 'col-xs-12',
h1 {}, 'Todo List'
TodoForm()
TodoList(todos: @state.todos)
Now we want to add an ajax call that is triggered by the form and hits the controller to add todos. First let's start with the controller create action.
# app/controllers/todos_controller.rb
class TodosController < ApplicationController
def index
@todos = Todo.all
end
def create
@todo = Todo.new(todo_params)
if @todo.save
render partial: 'todos/todo', locals: {todo: @todo}
else
render json: @todo.errors.to_json
end
end
private
def todo_params
params.require(:todo).permit(
:name,
:checked
)
end
end
Now let's update the component code so it hits this using ajax. Now when you type into the input and hit enter, it will create a todo through the todo controller.
# app/assets/javascripts/react/TodoIndex.js.coffee
{ div, h1, ul, li, a, span, label, input } = React.DOM
TodoForm = React.createFactory React.createClass
getInitialState: ->
todoName: ''
onInputChange: (e)->
@setState(todoName: e.target.value)
onInputKeyDown: (e)->
if e.keyCode == 13 && this.refs.todo.value.length
@props.submitTodo(this.refs.todo.value)
@setState(todoName: '')
render: ->
div className: 'form-group',
label {}, 'Enter Todo'
input
onChange: @onInputChange,
onKeyDown: @onInputKeyDown,
ref: 'todo',
className: 'form-control',
placeholder: 'Enter todo name'
value: @state.todoName
TodoListItem = React.createFactory React.createClass
render: ->
li className: 'list-item',
a className: 'btn btn-primary', 'Check'
span className: 'list-text', @props.todo.name
TodoList = React.createFactory React.createClass
render: ->
ul className: 'list-unstyled',
_.map @props.todos, (todo)=>
TodoListItem(todo: todo)
window.TodoIndex = React.createClass
getInitialState: ->
todos: []
componentWillMount: ->
@setState(todos: @props.todos)
componentWillMount: ->
TodoStore.listen(@onChange)
TodoActions.initData(@props)
componentWillUnmount: ->
TodoStore.unlisten(@onChange)
onChange: (state)->
@setState(state)
submitTodo: (name)->
$.ajax
type: 'POST'
url: '/todos'
data:
todo:
name: name
checked: false
success: (response)=>
@state.todos.push(response)
@setState(todos: @state.todos)
console.log(response)
error: (response)=>
console.log('error')
console.log(response)
render: ->
div className: 'container',
div className: 'row',
div className: 'col-xs-12',
h1 {}, 'Todo List'
TodoForm(submitTodo: @submitTodo)
TodoList(todos: @state.todos)
Add alt Actions and Stores to handle the flow of data
Now we can move onto adding alt. Alt is a flux framework that is really well made and simple. It takes a lot of boilerplate away and even cuts out the need for a dispatcher file. You can read more about it here, http://alt.js.org/. You're going to want to download it and include it in your vendor files along with lodash from before. Next we'll want to creat an initialize file for alt.
# app/assets/javascripts/initialize.js.coffee
window.alt = new Alt()
The components.js file will now look like this.
# app/assets/javascripts/components.js
//= require lodash/lodash.min
//= require alt/dist/alt.min
//= require initialize
//= require_tree ./react
Let's add a stores and actions folder under react and add a todo_store and todo_actions files. To start we are only going to move the initialize of data to the actions and stores.
# app/assets/javascripts/react/stores/todo_store.js.coffee
class TodoStore
@displayName: 'TodoStore'
constructor: ->
@bindActions(TodoActions)
@todos = []
@exportPublicMethods(
{
getTodos: @getTodos
}
)
onInitData: (props)->
@todos = props.todos
getTodos: ()->
@getState().todos
window.TodoStore = alt.createStore(TodoStore)
# app/assets/javascripts/react/actions/todo_actions.js.coffee
class TodoActions
constructor: ->
@generateActions(
'initData'
)
window.TodoActions = alt.createActions(TodoActions)
Now we need to update our TodoIndex component to properly listen to and initialize the data.
# app/assets/javascripts/react/TodoIndex.js.coffee
window.TodoIndex = React.createClass
getInitialState: ->
todos: []
componentWillMount: ->
TodoStore.listen(@onChange)
TodoActions.initData(@props)
componentWillUnmount: ->
TodoStore.unlisten(@onChange)
onChange: (state)->
@setState(state)
submitTodo: (name)->
$.ajax
type: 'POST'
url: '/todos'
data:
todo:
name: name
checked: false
success: (response)=>
@state.todos.push(response)
@setState(todos: @state.todos)
console.log(response)
error: (response)=>
console.log('error')
console.log(response)
render: ->
div className: 'container',
div className: 'row',
div className: 'col-xs-12',
h1 {}, 'Todo List'
TodoForm(submitTodo: @submitTodo)
TodoList(todos: @state.todos)
Your page should work just like before but now it's using Alt.js (flux)!!! Next we're going to move the submitTodos code to the actions/store code.
We will want to take out the submitTodo code from the top level component and place that in the store code. Then we'll add a 'submitTodo' action to the TodoActions. We'll be able to remove the passing of the submitTodo down from the top level component to the form which in the long run saves a lot of code. What generally happens is you find yourself passing functions all over the place when your app grows and flux lets you put the actions directly in the correct components and the components listening to those stores will automatically update.
# app/assets/javascripts/react/stores/todo_store.js.coffee
class TodoStore
#...
onSubmitTodo: (name)->
$.ajax
type: 'POST'
url: '/todos'
data:
todo:
name: name
checked: false
success: (response)=>
@todos.push(response)
@emitChange()
error: (response)=>
console.log('error')
console.log(response)
return
#...
# app/assets/javascripts/react/actions/todo_actions.js.coffee
class TodoActions
constructor: ->
@generateActions(
'initData',
'submitTodo'
)
window.TodoActions = alt.createActions(TodoActions)
# app/assets/javascripts/react/TodoIndex.js.coffee
{ div, h1, ul, li, a, span, label, input } = React.DOM
TodoForm = React.createFactory React.createClass
getInitialState: ->
todoName: ''
onInputChange: (e)->
@setState(todoName: e.target.value)
onInputKeyDown: (e)->
if e.keyCode == 13 && this.refs.todo.value.length
TodoActions.submitTodo(this.refs.todo.value)
@setState(todoName: '')
render: ->
div className: 'form-group',
label {}, 'Enter Todo'
input
onChange: @onInputChange,
onKeyDown: @onInputKeyDown,
ref: 'todo',
className: 'form-control',
placeholder: 'Enter todo name'
value: @state.todoName
# ...
window.TodoIndex = React.createClass
getInitialState: ->
todos: []
componentWillMount: ->
TodoStore.listen(@onChange)
TodoActions.initData(@props)
componentWillUnmount: ->
TodoStore.unlisten(@onChange)
onChange: (state)->
@setState(state)
render: ->
div className: 'container',
div className: 'row',
div className: 'col-xs-12',
h1 {}, 'Todo List'
TodoForm()
TodoList(todos: @state.todos)
Add check todo functionality
Last thing we're going to do is add a checkTodo action to check off the todos. First let's add the onClick handler to the TodoListItem that will hit the checkTodo action. We'll also add the code that adds the 'checked' class if it is checked in the database. I'll be showing all of the code since this is the last thing we're going to add.
# app/assets/javascripts/react/TodoIndex.js.coffee
{ div, h1, ul, li, a, span, label, input } = React.DOM
TodoForm = React.createFactory React.createClass
getInitialState: ->
todoName: ''
onInputChange: (e)->
@setState(todoName: e.target.value)
onInputKeyDown: (e)->
if e.keyCode == 13 && this.refs.todo.value.length
TodoActions.submitTodo(this.refs.todo.value)
@setState(todoName: '')
render: ->
div className: 'form-group',
label {}, 'Enter Todo'
input
onChange: @onInputChange,
onKeyDown: @onInputKeyDown,
ref: 'todo',
className: 'form-control',
placeholder: 'Enter todo name'
value: @state.todoName
TodoListItem = React.createFactory React.createClass
onCheckTodo: ->
TodoActions.checkTodo(@props.todo.id)
render: ->
todoItemClasses = 'list-item'
todoItemClasses += ' checked' if @props.todo.checked
li className: todoItemClasses,
a className: 'btn btn-primary', onClick: @onCheckTodo, 'Check'
span className: 'list-text', @props.todo.name
TodoList = React.createFactory React.createClass
render: ->
ul className: 'list-unstyled',
_.map @props.todos, (todo)=>
TodoListItem(key: "todo-#{todo.id}", todo: todo)
window.TodoIndex = React.createClass
getInitialState: ->
todos: []
componentWillMount: ->
TodoStore.listen(@onChange)
TodoActions.initData(@props)
componentWillUnmount: ->
TodoStore.unlisten(@onChange)
onChange: (state)->
@setState(state)
render: ->
div className: 'container',
div className: 'row',
div className: 'col-xs-12',
h1 {}, 'Todo List'
TodoForm()
TodoList(todos: @state.todos)
Add the checkTodo action.
# app/assets/javascripts/react/actions/todo_actions.js.coffee
class TodoActions
constructor: ->
@generateActions(
'initData',
'submitTodo',
'checkTodo'
)
window.TodoActions = alt.createActions(TodoActions)
Add the ajax call to 'delete' the todo in the store and emit the change to the components listening to it.
# app/assets/javascripts/react/stores/todo_store.js.coffee
class TodoStore
@displayName: 'TodoStore'
constructor: ->
@bindActions(TodoActions)
@todos = []
@exportPublicMethods(
{
getTodos: @getTodos
}
)
onInitData: (props)->
@todos = props.todos
onSubmitTodo: (name)->
console.log(name)
$.ajax
type: 'POST'
url: '/todos'
data:
todo:
name: name
checked: false
success: (response)=>
@todos.push(response)
@emitChange()
error: (response)=>
console.log('error')
console.log(response)
return false
onCheckTodo: (todo_id)->
$.ajax
type: 'DELETE'
url: "/todos/#{todo_id}"
success: (response)=>
_.find(@todos, { id: response.id} ).checked = true
@emitChange()
error: (response)=>
console.log('error')
console.log(response)
return false
getTodos: ()->
@getState().todos
window.TodoStore = alt.createStore(TodoStore)
We'll need to add a custom 'destroy' method to the todos_controller so it just updates it with checked: true.
# app/controllers/todos_controller.rb
class TodosController < ApplicationController
def index
@todos = Todo.all
end
def create
@todo = Todo.new(todo_params)
if @todo.save
render partial: 'todos/todo', locals: {todo: @todo}
else
render json: @todo.errors.to_json
end
end
def destroy
@todo = Todo.find(params[:id])
if @todo.update(checked: true)
render partial: 'todos/todo', locals: {todo: @todo}
else
render json: @todo.errors.to_json
end
end
private
def todo_params
params.require(:todo).permit(
:name,
:checked
)
end
end
Run your server and you are done! You can click the buttons and it will check the todos you add. Now you can easily add flux and react into your rails app without having to deal with things like npm and webpack.
Conclusion
I have loved using react in almost all of my projects, but I still love the utility of rails. I think this setup is one of the quickest ways for a rails dev to start playing with react code. You'll quickly find that without a flux implementation that you can't scale your react components and have them logically interact without a lot of extra code. This tutorial is a nice and simple guide that shows you how to add on flux and continue using sprockets for your react code.
Now if you are an advanced user of react and still want to integrate that into your pipeline, I think this gem isn't the right tool for you. I have been using redux a lot more lately and to really leverage all of the tooling around it, you need to be using npm. This also leads you down a path of using tools like webpack to help stay on the same ecosystem as the node developers building out the react ecosystem. If you are going down this route and still want to integrate this code into your rails app, I highly recommend the react_on_rails gem, https://github.com/shakacode/react_on_rails.
Good luck in your react adventures!!!