React-Rails with Flux (Alt.js) Tutorial

By Blaine Hatab | October 31, 2015

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!!!

comments powered byDisqus