Development

Rails and React: Forms, Validations and Real-time Updates

By August 4, 2016 No Comments

Over the last few years technologies and frameworks built around ‘real time’ web applications have become incredibly popular. Javascript libraries and frameworks such as Angular, React, Ember, Meteor and ExtJS are some of the better known examples. In this post we will look at how to use and implement the React javascript library in Rails via the ‘react-rails’ gem. We’ll go beyond a simple React project set up and look at how to communicate with a simple Rails api controller. We’ll also look at how to implement forms in React that work together with Rails validations.

General Considerations and Use Cases

Before deciding to implement ‘real time’ features into your application, it’s worth taking some time to consider the needs and specifications of your project. Think about if you actually need these features and if so, think carefully about why. These questions can help you make an informed decision about which approach and technology to use. Projects which MUST be implemented entirely as a single page application are quite rare. Therefore in most cases you’ll have some freedom and options available to you. Are you just looking for faster load times and a more fluid user experience? If so turbolinks should give you mostly what you’re looking for. Is there a page or section where you need a ‘real time’ feature like dynamic search? In this case you can probably accomplish this by adding some Ajax to just that part of your rails application via the Rails jquery_ujs helpers.

React and Rails

React is a javascript library that is focused on the view layer. It is much easier to integrate into a Rails application than a framework like Angular and we can choose to use React only in the places where we need it. It is mostly agreed that React has a relatively low learning curve and is generally quite forgiving which makes it nice to work with.

React is built to help us manage complex views consisting of many interrelated components that will all be updating in response to user interaction. In Rails trying to manage such a view on a single page via Ajax can quickly become unwieldy. The state of your view can become decentralized across a variety of instance variables and DOM elements. To update the view you’ll find yourself traversing a confusing set of if-else blocks and event handlers. React splits our view up across localized components. Each of these components can manage it’s own state and are hooked up to each other via a uni-directional data flow inherent to the React library. Your view becomes better organized both from a code perspective and a conceptual perspective.

An introduction to React

One thing to keep in mind is that when you decide to use React for a view, you lose all of the awesome Rails view templates and helpers. What this means is you end up writing a lot more markup and code then you would with a conventional Rails view. This will end up taking you a lot more time, especially if you are new to React. When starting I often lamented the lack of haml and simple form.

A sample Rails application with React

Let’s start by thinking about a specific project where it seems like React might be helpful. A client wants to build a project management app. Each project will have a name, description and a project cost. You start by building out some conventional CRUD wireframes. Unfortunately the client is not happy. When creating a new project or editing an existing project he wants users to be able to see the total cost of all projects and the cost of each other project. He doesn’t like that a new page is rendered for these actions, he wants to stay on the initial index listing of projects while still being able to create and edit projects. Also, the total project cost should update automatically in response to changes. It certainly seems like we’ll be needing some ‘single page application’ features for this page.

We’ll use Rails 5 and the react-rails gem. The react-rails gem integrates with Rails and the asset pipeline. It also provides us with some other nice things like view helpers and generators. Integrating React into Rails via the react-rails gem is quick and simple. Some developers prefer to separate the front and back end environments entirely, using Webpack and Node for the React front end.

This approach is described in more detail here

There are also ‘hybrid’ implementations that integrate Webpack and the javascript asset toolchain directly into Rails and sprockets.

The react_on_rails gem utilizes the above approach

Project Set Up

You can find the source code of the completed application here.

View the project on github

Let’s get started by making a new rails project.

rails new project_management_react

We’ll use postgres for our database.

gem 'pg'

Replace the database.yml file with the following

development:
  adapter: postgresql
  encoding: unicode
  database: project_management_development
  pool: 5
  min_messages: WARNING

test:
  adapter: postgresql
  encoding: unicode
  database: project_management_test
  pool: 5

We’ll use node for our javascript environment. Install node on your machine if you don’t already have it.

We’ll use haml for our view templates although we won’t really be using it here, React will be responsible for rendering our views.

gem 'haml-rails'

Run the following generator after you bundle.

rake haml:erb2haml

Replace the existing .erb views.

We’ll use bootstrap for our css.

gem 'bootstrap-sass', '~> 3.3.6'

Make sure you also have the following gem which should be already included

gem 'sass-rails', '~> 5.0'

Replace your .css stylesheet with .scss and add the following import directives to the top.

@import "bootstrap-sprockets";
@import "bootstrap";

Add the following to your application.js.

//= require bootstrap-sprockets

Put this directly before the require tree statement.

Create a scaffold for projects

rails g scaffold Project name:string description:text project_cost:decimal

Change the migration for Project as follows.

t.decimal :project_cost, precision: 8, scale: 2

Run the migration.

Add a root path to routes.

root to: "projects#index"

Run your server and you should arrive successfully at projects.html.haml

Lastly, we’ll add React. Add the react-rails gem

gem 'react-rails'

Bundle and run the generator

rails g react:install

This should add react to your application.js and create a componenets directory under your javascript assets folder.

Continuing on, you may find the official React tutorial and guides helpful.

Building the application with React

Let’s start by creating our first React component which I will call ProjectsContainer. We can use the generator included by react-rails.

rails generate react:component ProjectsContainer

You should see the following in the newly created projects_container.js.jsx file.

var ProjectsContainer = React.createClass({
  render: function() {
    return <div />;
  }
});

All React componenets have a render method. The html returned in the render method is what will be rendered for that component. All React components also have properties (‘props’) and ‘state’. A componenet’s state is an object that represents the component’s current state. We can set whatever variables we need to represent the state of our component. When a component’s state is changed, the component will re-render itself. A component’s ‘props’ are passed in by the component’s parent. In the case of a top level component, ‘props’ can be passed in by the react_component method when using the react-rails gem. A component’s ‘props’ object is immutable. Whenever possible our components should render based on what is passed to them via ‘props’. By setting up our components this way we respect React’s ‘top down’ uni directional data flow. This makes things easier since we don’t need to worry about knowing and updating each component’s individual state. In practice, this approach can be difficult to implement perfectly. Try to make your components ‘stateless’ where possible.

Let’s start by rendering our ProjectsContainer component from our project index.html.haml. In this project, our entire view and all of the functionality will be defined in this component

= react_component("ProjectsContainer")

We are not passing anything to the ProjectsContainer. Therefore ProjectsContainer will have no ‘props’.

Let’s render a header from the ProjectsContainer component

var ProjectsContainer = React.createClass({
  render: function() {
    return(
      <div>
        <h1> Project List </h1>
      </div>
    );
  }
});

Now we should see the ‘Project List’ header in our index view. Next we are going to add more components. Our ProjectsContainer will contain a list of projects, a new project form and a disply for the total project cost. Let’s create the following components…

ProjectTable NewProjectForm ProjectCost

After creating these components, add them to the ProjectsContainer component as follows.

var ProjectsContainer = React.createClass({
  render: function() {

    return(
      <div>

        <h1> Project List </h1>

        <ProjectCost  />

        <ProjectTable />

        <NewProjectForm />

      </div>
    );
  }
});

Jsx allows us to render components as shown above. The render method for each of these components will be called.

We want to render our list of projects in ProjectTable. To do this we will need to get our projects from the rails server. In the index action of our projects controller we are already loading our projects into the @projects instance variable. We need to pass this as a prop to ProjectsContainer and from there down to ProjectTable.

= react_component("ProjectsContainer", {projects: @projects})
var ProjectsContainer = React.createClass({
  getInitialState: function(){
    return {
      projects: this.props.projects,
    }
  },

  render: function() {

    return(
      <div>

        <h1> Project List </h1>

        <ProjectCost  />

        <ProjectTable projects={this.state.projects} />

        <NewProjectForm />

      </div>
    );
  }
});

Notice how in the projects container we define the getInitialState method. This is a React method and is used to define the state when the React component is first mounted. Generally we want to set our state to be the same as the ‘props’ that were passed in. The getInitialState method will only be called when the component is first mounted to the page. It will not be called on every render. If a component is removed from the page and later re-added, this method will be called again. You can read more about the React component lifecycle and the associated methods here.

React component lifecycle

Notice how within a React component we can access the ‘props’ via this.props and the state via this.state. We pass projects from the state of ProjectContainer to the ‘props’ of ProjectTable via an attribute. Within ProjectTable we will now be able to access projects via this.props.projects. Within jsx markup, curly braces are necessary to execute Javascript/React code. Now that our projects can be accessed by ProjectTable, let’s render a list of of our projects.

var ProjectTable = React.createClass({


  renderProjectRows: function(){
    return(
      this.props.projects.map(function(project){
        return(
          <div className="row" style={{marginTop: "20px"}} key={project.id}>

            <div className="col-sm-2">
            </div>

            <div className="col-sm-2">
              {project.name}
            </div>

            <div className="col-sm-4">
              {project.description}
            </div>

            <div className="col-sm-2">
              {project.project_cost}
            </div>

          </div>
        )
      })
    );
  },

  render: function() {
    return(
      <div>

        <div className="row" style={{marginTop: "50px"}}>

          <div className="col-sm-2">
          </div>

          <div className="col-sm-2" style={{fontWeight: "bold"}}>
            Name
          </div>

          <div className="col-sm-4" style={{fontWeight: "bold"}}>
            Description
          </div>

          <div className="col-sm-2" style={{fontWeight: "bold"}}>
            Project Cost
          </div>

        </div>

        {this.renderProjectRows()}

      </div>
    );
  }

});

Here we’ve set up a table layout using bootstrap classes which we set on our divs via the className property. Notice how we’ve split the rendering across two functions. The rows for each project are rendered from the renderProjectRows function which utilizes the javascript map function to iterate over this.props.projects. When creating multiple rows via the map function, we set a React key for each row so React can uniquely identify each row. It uses this key in deciding how to update the Virtual DOM with each new render.

More about React Multiple Components

The table ‘header’ is defined in the render function. We then call our renderProjectRows function via {this.renderProjectRows()}. Finally let’s look at the inline styles defined on our rows. We have already seen some examples of ways in which jsx is unique. Here is another example, styles are defined via an object with the CSS attributes changed to camelcase. We define our object as {marginTop: “10px”} and then wrap the javascript object in another set of braces so that it can be parsed as jsx.

You can use your rails console to make a new project and confirm that it shows up in your view.

New and Create in React

Let’s now add our new project form.

Our Rails project model has three fields, name, description and project_cost. We’ll start by making our NewProjectForm component render a form with those three fields. In React we have to do a little bit of extra work to ensure that the value of each input as it is changed in the DOM, is successfully updated in the React component state.

var NewProjectForm = React.createClass({


  getInitialState: function(){
    return {name: "", description: "", project_cost: ""};
  },

  newProjectSubmit: function(e){
    e.preventDefault();

    console.log("submitted");
  },

  handleNameChange: function(e){
    this.setState({name: e.target.value});
  },

  handleDescriptionChange: function(e){
    this.setState({description: e.target.value});
  },

  handleProjectCostChange: function(e){
    this.setState({project_cost: e.target.value});
  },


  renderProjectNameField: function(){


    return(

      <div className='row'>

        <div className='col-sm-4'>

          <div className= 'form-group'>

            <input
              name="project[name]"
              type="string"
              placeholder="Project Name"
              value={this.state.name}
              onChange={this.handleNameChange}
              className="string form-control"
            />

          </div>

        </div>

      </div>
    );
  },

  renderProjectDescriptionField: function(){


    return(
      <div className='row'>

        <div className='col-sm-4'>

          <div className= 'form-group'>

            <textarea
              name="project[description]"
              placeholder="Project Description"
              value={this.state.description}
              onChange={this.handleDescriptionChange}
              className="text form-control"
            />

          </div>

        </div>

      </div>
    );

  },

  renderProjectCostField: function(){

    return(
      <div className='row'>

        <div className='col-sm-4'>

          <div className='form-group'>

            <input
              name="project[project_cost]"
              type="number"
              placeholder="Project Cost"
              value={this.state.project_cost}
              onChange={this.handleProjectCostChange}
              className="numeric decimal form-control"
            />

          </div>

        </div>
      </div>
    );

  },



  render: function() {

    return(
      <div>
        <h4 style={{marginTop: "50px"}}> Create New Project </h4>

        <form style={{marginTop: "30px"}} onSubmit={this.newProjectSubmit}>

          <div className='form-inputs'/>


            {this.renderProjectNameField()}

            {this.renderProjectDescriptionField()}

            {this.renderProjectCostField()}


            <div className='row'>
              <div className='col-sm-4'>
                <input type="submit" value="Save" className='btn btn-primary' />
              </div>
            </div>

        </form>

      </div>

    );
  }
});

Our new project form component has three state variables, name, description and project_cost. Since this is a form for a new project, in the getInitialState method we start by setting each of these to the empty string. We have a render function for each of our inputs which are each called in the final render method. For each of these inputs we have duplicated the layout and classes that we would get using a Rails templated form with bootstrap. Each input has an associated onChange handler function that updates the state when the input value is changed. These onChange functions keep the React component state in sync with the input values. We define a function newProjectSubmit method and pass it to the onSubmit attribute of our form. Right now our newProjectSubmit method only calls console.log.

More about React forms and controlled components

Now let’s actually write the submit method. We’re going to start by assuming that whatever we submit will be successfully saved. Later on, we will add validations to our Rails Project model and look at how to handle validation errors on the React side. Let’s think about what we want to happen when we create a new project. We obviously want our new project to be created. We also want our projects list to automatically refresh and our new project list to clear it’s values. Since we want our new project list to refresh we will need to hook up our project submit method to the top level ProjectsContainer since this is where the ‘projects’ state is located. This is a common way of doing things in React and reflects the data flow pattern of React. Top level components handle updates and propogate changes down. In this case, ProjectContainer will submit the new project to the Rails create method, get the projects as returned from Rails and pass the new projects down to the ProjectTable.

Let’s start by looking at our Rails controller. We will be executing an Ajax call to the create method. If create is successful we will want to return the projects to React as JSON data.

class ProjectsController < ApplicationController
  before_action :set_project, only: [:show, :edit, :update, :destroy]


  def index
    @projects = Project.all.order(:name)
  end


  def show
  end

  def new
    @project = Project.new
  end

  def edit
  end


  def create
    @project = Project.new(project_params)

    respond_to do |format|
      if @project.save
        format.html { redirect_to @project, notice: 'Project was successfully created.' }

        format.json { render json: Project.all.order(:name) }

      else
        format.html { render :new }
      end
    end
  end

  def update
    respond_to do |format|
      if @project.update(project_params)
        format.html { redirect_to @project, notice: 'Project was successfully updated.' }
      else
        format.html { render :edit }
      end
    end
  end


  def destroy
    @project.destroy
    respond_to do |format|
      format.html { redirect_to projects_url, notice: 'Project was successfully destroyed.' }
    end
  end

  private
    def set_project
      @project = Project.find(params[:id])
    end

    def project_params
      params.require(:project).permit(:name, :description, :project_cost)
    end
end

Let’s add our ajax submit method to ProjectContainer. Remember that this method is going to be called by NewProjectForm. Therefore we will pass this method down to NewProjectForm as a prop.

var ProjectsContainer = React.createClass({

  getInitialState: function(){
    return {
      projects: this.props.projects,
    }
  },

  parentProjectSubmit: function(formData){

    $.ajax({
      url: "/projects",
      dataType: 'json',
      type: 'POST',
      data: formData,

      success: function(projects) {

        this.setState({projects: projects});

      }.bind(this),

      error: function(response, status, err) {

        console.log("An error occured")
      }


    });
  },

  render: function() {

    return(
      <div>

        <h1> Project List </h1>

        <ProjectCost  />

        <ProjectTable projects={this.state.projects} />

        <NewProjectForm parentProjectSubmit={this.parentProjectSubmit} />

      </div>
    );
  }
});

Our parentProjectSubmit method expects formData to be given to it by NewProjectForm. In the success callback we run the setState method to update the ProjectContainer state to the new projects returned by Rails. Here we must explicitly bind ‘this’ to ProjectContainer. Otherwise ‘this’ will refer to NewProjectForm where the method will actually be called.

Now that we’ve defined this method and passed it to NewProjectForm, let’s call the method in our newProjectSubmit function.

var NewProjectForm = React.createClass({


  getInitialState: function(){
    return {name: "", description: "", project_cost: ""};
  },

  newProjectSubmit: function(e){
    e.preventDefault();

    this.props.parentProjectSubmit({project: {name: this.state.name, description: this.state.description, project_cost: this.state.project_cost}});
  },

  handleNameChange: function(e){
    this.setState({name: e.target.value});
  },

  handleDescriptionChange: function(e){
    this.setState({description: e.target.value});
  },

  handleProjectCostChange: function(e){
    this.setState({project_cost: e.target.value});
  },


  renderProjectNameField: function(){


    return(

      <div className='row'>

        <div className='col-sm-4'>

          <div className= 'form-group'>

            <input
              name="project[name]"
              type="string"
              placeholder="Project Name"
              value={this.state.name}
              onChange={this.handleNameChange}
              className="string form-control"
            />

          </div>

        </div>

      </div>
    );
  },

  renderProjectDescriptionField: function(){


    return(
      <div className='row'>

        <div className='col-sm-4'>

          <div className= 'form-group'>

            <textarea
              name="project[description]"
              placeholder="Project Description"
              value={this.state.description}
              onChange={this.handleDescriptionChange}
              className="text form-control"
            />

          </div>

        </div>

      </div>
    );

  },

  renderProjectCostField: function(){

    return(
      <div className='row'>

        <div className='col-sm-4'>

          <div className='form-group'>

            <input
              name="project[project_cost]"
              type="number"
              placeholder="Project Cost"
              value={this.state.project_cost}
              onChange={this.handleProjectCostChange}
              className="numeric decimal form-control"
            />

          </div>

        </div>
      </div>
    );

  },



  render: function() {

    return(
      <div>
        <h4 style={{marginTop: "50px"}}> Create New Project </h4>

        <form style={{marginTop: "30px"}} onSubmit={this.newProjectSubmit}>

          <div className='form-inputs'/>


            {this.renderProjectNameField()}

            {this.renderProjectDescriptionField()}

            {this.renderProjectCostField()}


            <div className='row'>
              <div className='col-sm-4'>
                <input type="submit" value="Save" className='btn btn-primary' />
              </div>
            </div>

        </form>

      </div>

    );
  }
});

The form data we submit to Rails must match the format expected by the Rails Projects controller. For this reason we pass the following object to our parentProjectSubmit method. {project: {name: this.state.name, description: this.state.description, project_cost: this.state.project_cost}}

Now when we push the save button on our new project form, the new project will be created and the project list gets re rendered.

There are two things left to do for the NewProjectForm. First we need to clear the state of our form when a new project is successfully created. Second we need to add validations and handle validation errors. Let’s start with the first item.

To clear the new project form we are going to have our parentProjectSubmit method accept a function that will be called when the ajax post is successful. This success callback will clear the state of our NewProjectForm. Since the success callback will be defined in NewProjectForm where parentProjectSubmit is being executed, we don’t have to worry about binding the ‘this’ context, it will already correctly refer to the NewProjectForm component. Here’s our ProjectsContainer and NewProjectForm using the new success callback, ‘resetState’.

var ProjectsContainer = React.createClass({

  getInitialState: function(){
    return {
      projects: this.props.projects,
    }
  },

  parentProjectSubmit: function(formData, onSuccess){

    $.ajax({
      url: "/projects",
      dataType: 'json',
      type: 'POST',
      data: formData,

      success: function(projects) {

        this.setState({projects: projects});

        onSuccess();

      }.bind(this),

      error: function(response, status, err) {

        console.log("An error occured")
      }


    });
  },

  render: function() {

    return(
      <div>

        <h1> Project List </h1>

        <ProjectCost  />

        <ProjectTable projects={this.state.projects} />

        <NewProjectForm parentProjectSubmit={this.parentProjectSubmit} />

      </div>
    );
  }
});

var NewProjectForm = React.createClass({


  getInitialState: function(){
    return {name: "", description: "", project_cost: ""};
  },

  resetState: function(){
    this.setState({name: "", description: "", project_cost: ""});
  },

  newProjectSubmit: function(e){
    e.preventDefault();

    this.props.parentProjectSubmit({project: {name: this.state.name, description: this.state.description, project_cost: this.state.project_cost}}, this.resetState);
  },

  handleNameChange: function(e){
    this.setState({name: e.target.value});
  },

  handleDescriptionChange: function(e){
    this.setState({description: e.target.value});
  },

  handleProjectCostChange: function(e){
    this.setState({project_cost: e.target.value});
  },


  renderProjectNameField: function(){


    return(

      <div className='row'>

        <div className='col-sm-4'>

          <div className= 'form-group'>

            <input
              name="project[name]"
              type="string"
              placeholder="Project Name"
              value={this.state.name}
              onChange={this.handleNameChange}
              className="string form-control"
            />

          </div>

        </div>

      </div>
    );
  },

  renderProjectDescriptionField: function(){


    return(
      <div className='row'>

        <div className='col-sm-4'>

          <div className= 'form-group'>

            <textarea
              name="project[description]"
              placeholder="Project Description"
              value={this.state.description}
              onChange={this.handleDescriptionChange}
              className="text form-control"
            />

          </div>

        </div>

      </div>
    );

  },

  renderProjectCostField: function(){

    return(
      <div className='row'>

        <div className='col-sm-4'>

          <div className='form-group'>

            <input
              name="project[project_cost]"
              type="number"
              placeholder="Project Cost"
              value={this.state.project_cost}
              onChange={this.handleProjectCostChange}
              className="numeric decimal form-control"
            />

          </div>

        </div>
      </div>
    );

  },



  render: function() {

    return(
      <div>
        <h4 style={{marginTop: "50px"}}> Create New Project </h4>

        <form style={{marginTop: "30px"}} onSubmit={this.newProjectSubmit}>

          <div className='form-inputs'/>


            {this.renderProjectNameField()}

            {this.renderProjectDescriptionField()}

            {this.renderProjectCostField()}


            <div className='row'>
              <div className='col-sm-4'>
                <input type="submit" value="Save" className='btn btn-primary' />
              </div>
            </div>

        </form>

      </div>

    );
  }
});

Now when a new project is submitted, the new project form is successfully cleared.

Forms and Validtion Errors in React

For handling validation errors, we will take a similar approach and define an error callback. This error callback will pass the returned error object to the new project form. The new project form will then update it’s state. We will change the view markup to render these errors as we would expect in a Rails form template.

We need to start by returning the errors in our Rails create method. Remember to also add some validations to your Project model.

def create
  @project = Project.new(project_params)

  respond_to do |format|
    if @project.save
      format.html { redirect_to @project, notice: 'Project was successfully created.' }

      format.json { render json: Project.all.order(:name) }

    else
      format.html { render :new }

      format.json { render json: @project.errors, status: :unprocessable_entity }

    end
  end
end

Let’s again update our ProjectContainer and NewProjectForm. This time we will add an error callback. We will also add a formErrors object to our NewProjectContainer state. Finally we’ll add a render errors method and update our render input methods. Lastly, don’t forget to clear the formErrors in the NewProjectForm resetState method.

var ProjectsContainer = React.createClass({

  getInitialState: function(){
    return {
      projects: this.props.projects,
    }
  },

  parentProjectSubmit: function(formData, onSuccess, onError){

    $.ajax({
      url: "/projects",
      dataType: 'json',
      type: 'POST',
      data: formData,

      success: function(projects) {

        this.setState({projects: projects});

        onSuccess();

      }.bind(this),

      error: function(response, status, err) {

        onError(response.responseJSON)

      }


    });
  },

  render: function() {

    return(
      <div>

        <h1> Project List </h1>

        <ProjectCost  />

        <ProjectTable projects={this.state.projects} />

        <NewProjectForm parentProjectSubmit={this.parentProjectSubmit} />

      </div>
    );
  }
});
var NewProjectForm = React.createClass({


  getInitialState: function(){
    return {name: "", description: "", project_cost: "", formErrors: {}};
  },

  resetState: function(){
    this.setState({name: "", description: "", project_cost: "", formErrors: {}});
  },

  handleValidationError: function(formErrorObj){
    this.setState({formErrors: formErrorObj});
  },

  newProjectSubmit: function(e){
    e.preventDefault();

    this.props.parentProjectSubmit(
      {project: {name: this.state.name, description: this.state.description, project_cost: this.state.project_cost}},
      this.resetState,
      this.handleValidationError
    );
  },

  handleNameChange: function(e){
    this.setState({name: e.target.value});
  },

  handleDescriptionChange: function(e){
    this.setState({description: e.target.value});
  },

  handleProjectCostChange: function(e){
    this.setState({project_cost: e.target.value});
  },

  renderFieldErrors: function(attribute){
    if(this.state.formErrors[attribute]){
      return(
        this.state.formErrors[attribute].map(function(error, i){
          return(
            <span key={i} className="help-block">
              {error}
            </span>
          );
        })
      );
    }
    else{
      return "";
    }
  },


  renderProjectNameField: function(){

    var formGroupClass = this.state.formErrors["name"] ? "form-group has-error" : "form-group"

    return(

      <div className='row'>

        <div className='col-sm-4'>

          <div className= {formGroupClass}>

            <input
              name="project[name]"
              type="string"
              placeholder="Project Name"
              value={this.state.name}
              onChange={this.handleNameChange}
              className="string form-control"
            />

            {this.renderFieldErrors("name")}

          </div>

        </div>

      </div>
    );
  },

  renderProjectDescriptionField: function(){

    var formGroupClass = this.state.formErrors["description"] ? "form-group has-error" : "form-group"

    return(
      <div className='row'>

        <div className='col-sm-4'>

          <div className= {formGroupClass}>

            <textarea
              name="project[description]"
              placeholder="Project Description"
              value={this.state.description}
              onChange={this.handleDescriptionChange}
              className="text form-control"
            />

            {this.renderFieldErrors("description")}

          </div>

        </div>

      </div>
    );

  },

  renderProjectCostField: function(){

    var formGroupClass = this.state.formErrors["project_cost"] ? "form-group has-error" : "form-group"

    return(
      <div className='row'>

        <div className='col-sm-4'>

          <div className= {formGroupClass}>

            <input
              name="project[project_cost]"
              type="number"
              placeholder="Project Cost"
              value={this.state.project_cost}
              onChange={this.handleProjectCostChange}
              className="numeric decimal form-control"
            />

            {this.renderFieldErrors("project_cost")}

          </div>

        </div>
      </div>
    );

  },



  render: function() {

    return(
      <div>
        <h4 style={{marginTop: "50px"}}> Create New Project </h4>

        <form style={{marginTop: "30px"}} onSubmit={this.newProjectSubmit}>

          <div className='form-inputs'/>


            {this.renderProjectNameField()}

            {this.renderProjectDescriptionField()}

            {this.renderProjectCostField()}


            <div className='row'>
              <div className='col-sm-4'>
                <input type="submit" value="Save" className='btn btn-primary' />
              </div>
            </div>

        </form>

      </div>

    );
  }
});

Now our new project form successfully renders validations errors returned to it by Rails. At this point you are probably starting to get a feel for how much extra code and markup we have to write compared to Rails. Hopefully you are also starting to notice how well connected each React component feels and how this makes it easier to control and manage the state of our view.

Edit and Update in React

Now we’re going to add the ability for the user to edit and update each project. Since we will now be editing and updating each project row, we will make each row into a separate React component with it’s own state. ProjectTable will then render the project rows as React componenets. Each project row will have a state variable which determines if the project is currently being edited or not. Depending on the value of this variable, the project row will render as either a form or just a row. Much of the remaining logic will be the same as what we’ve already done for the NewProjectForm.

Let’s start with the update method of our Rails controller.

def update
  respond_to do |format|
    if @project.update(project_params)
      format.html { redirect_to @project, notice: 'Project was successfully updated.' }
      format.json { render json: Project.all.order(:name) }
    else
      format.html { render :edit }
      format.json { render json: @project.errors, status: :unprocessable_entity }
    end
  end
end

Next we’ll make the previously mentioned updates to ProjectContainer and ProjectTable. We will also create a new component, ProjectRow.

var ProjectsContainer = React.createClass({

  getInitialState: function(){
    return {
      projects: this.props.projects,
    }
  },

  parentProjectSubmit: function(formData, onSuccess, onError){

    $.ajax({
      url: "/projects",
      dataType: 'json',
      type: 'POST',
      data: formData,

      success: function(projects) {

        this.setState({projects: projects});

        onSuccess();

      }.bind(this),

      error: function(response, status, err) {

        onError(response.responseJSON)

      }


    });
  },

  parentUpdateProject: function(formData, onSuccess, onError){

    $.ajax({
      url: ("/projects/" + formData["project"]["id"]),
      dataType: 'json',
      type: 'PATCH',
      data: formData,


      success: function(projects) {

        this.setState({projects: projects, showNewForm: false});
        onSuccess();

      }.bind(this),

      error: function(response, status, err) {

        onError(response.responseJSON)
      }

    });

  },

  render: function() {

    return(
      <div>

        <h1> Project List </h1>

        <ProjectCost  />

        <ProjectTable projects={this.state.projects} parentUpdateProject={this.parentUpdateProject} />

        <NewProjectForm parentProjectSubmit={this.parentProjectSubmit} />

      </div>
    );
  }
});

var ProjectTable = React.createClass({


  renderProjectRows: function(){

    return (
      this.props.projects.map(function(project){
        return(
          <ProjectRow
            key={project.id}
            id={project.id}
            name={project.name}
            description={project.description}
            project_cost={project.project_cost}
            parentUpdateProject={this.props.parentUpdateProject} />
        );
      }.bind(this))
    );

  },

  render: function() {
    return(
      <div>

        <div className="row" style={{marginTop: "50px"}}>

          <div className="col-sm-2">
          </div>

          <div className="col-sm-2" style={{fontWeight: "bold"}}>
            Name
          </div>

          <div className="col-sm-4" style={{fontWeight: "bold"}}>
            Description
          </div>

          <div className="col-sm-2" style={{fontWeight: "bold"}}>
            Project Cost
          </div>

        </div>

        {this.renderProjectRows()}

      </div>
    );
  }

});



var ProjectRow = React.createClass({

  getInitialState: function(){
    return ( {id: this.props.id, name: this.props.name, description: this.props.description, project_cost: this.props.project_cost, edit: false, formErrors: {}} )
  },

  editProject: function(){
    this.setState({edit: true});
  },

  cancelEdit: function(e){
    e.preventDefault();
    this.setState({edit: false, name: this.props.name, description: this.props.description, project_cost: this.props.project_cost, formErrors: {}});
  },

  handleNameChange: function(e){
    this.setState({name: e.target.value});
  },

  handleDescriptionChange: function(e){
    this.setState({description: e.target.value});
  },

  handleProjectCostChange: function(e){
    this.setState({project_cost: e.target.value});
  },

  handleValidationErrors: function(formErrorObject){
    this.setState({edit: true, formErrors: formErrorObject});
  },

  handleUpdate: function(){
    this.setState({edit: false, formErrors: false});
  },

  updateProject: function(e){
    e.preventDefault();
    this.props.parentUpdateProject(
      {project: {id: this.state.id, name: this.state.name, description: this.state.description, project_cost: this.state.project_cost}},
      this.handleUpdate,
      this.handleValidationErrors
    );
  },

  renderFieldErrors: function(attribute){
    if(this.state.formErrors[attribute]){
      return(
        this.state.formErrors[attribute].map(function(error, i){
          return(
            <span key={i} className="help-block">
              {error}
            </span>
          );
        })
      );
    }
    else{
      return "";
    }
  },

  renderProjectNameEditFields: function(){
    var formGroupClass = this.state.formErrors["name"] ? "form-group has-error" : "form-group"

    return(

      <div className= {formGroupClass}>

        <input
          name="project[name]"
          type="string"
          placeholder="Project Name"
          value={this.state.name}
          onChange={this.handleNameChange}
          className="string form-control"
        />

        {this.renderFieldErrors("name")}

      </div>


    );
  },

  renderProjectDescriptionEditFields: function(){
    var formGroupClass = this.state.formErrors["description"] ? "form-group has-error" : "form-group"

    return(

      <div className= {formGroupClass}>

        <textarea
          name="project[description]"
          placeholder="Project Description"
          value={this.state.description}
          onChange={this.handleDescriptionChange}
          className="text form-control"
        />

        {this.renderFieldErrors("description")}

      </div>


    );
  },

  renderProjectCostEditFields: function(){

    var formGroupClass = this.state.formErrors["project_cost"] ? "form-group has-error" : "form-group"

    return(

      <div className={formGroupClass}>

        <input
          name="project[project_cost]"
          type="number"
          placeholder="Project Cost"
          value={this.state.project_cost}
          onChange={this.handleProjectCostChange}
          className="numeric decimal form-control"
        />

        {this.renderFieldErrors("project_cost")}

      </div>


    );

  },

  render: function() {

    if(this.state.edit == false){
      return(
        <div className="row" style={{marginTop: "20px"}}>

          <div className="col-sm-2">
            <button className='btn btn-sm btn-primary' onClick={this.editProject}>
              Edit Project
            </button>
          </div>

          <div className="col-sm-2">
            {this.state.name}
          </div>

          <div className="col-sm-4">
            {this.state.description}
          </div>

          <div className="col-sm-2">
            {this.state.project_cost}
          </div>

        </div>
      );
    }
    else{
      return(

        <div className="row" style={{marginTop: "20px"}}>

          <form style={{marginTop: "30px"}} onSubmit={this.updateProject}>

            <div className="col-sm-2">

              <input type="submit" value="Save" className='btn btn-success' />

              <button className='btn btn-sm btn-primary' style={{marginLeft:'10px'}} onClick={this.cancelEdit}>
                Cancel
              </button>

            </div>

            <div className="col-sm-2">
              {this.renderProjectNameEditFields()}
            </div>

            <div className="col-sm-4">
              {this.renderProjectDescriptionEditFields()}
            </div>

            <div className="col-sm-2">
              {this.renderProjectCostEditFields()}
            </div>

          </form>

        </div>


      );
    }

  }

});

Finishing Up

The last thing we will add to our project list is the Total Project cost display. Since we already have all of our projects synced to the top level ProjectsContainer, adding this new component will be easy. We’ll pass in projects to our ProjectCost component and update the component accordingly.

var ProjectsContainer = React.createClass({

  getInitialState: function(){
    return {
      projects: this.props.projects,
    }
  },

  parentProjectSubmit: function(formData, onSuccess, onError){

    $.ajax({
      url: "/projects",
      dataType: 'json',
      type: 'POST',
      data: formData,

      success: function(projects) {

        this.setState({projects: projects});

        onSuccess();

      }.bind(this),

      error: function(response, status, err) {

        onError(response.responseJSON)

      }


    });
  },

  parentUpdateProject: function(formData, onSuccess, onError){

    $.ajax({
      url: ("/projects/" + formData["project"]["id"]),
      dataType: 'json',
      type: 'PATCH',
      data: formData,


      success: function(projects) {

        this.setState({projects: projects, showNewForm: false});
        onSuccess();

      }.bind(this),

      error: function(response, status, err) {

        onError(response.responseJSON)
      }

    });

  },

  render: function() {

    return(
      <div>

        <h1> Project List </h1>

        <ProjectCost projects={this.state.projects} />

        <ProjectTable projects={this.state.projects} parentUpdateProject={this.parentUpdateProject} />

        <NewProjectForm parentProjectSubmit={this.parentProjectSubmit} />

      </div>
    );
  }
});

var ProjectCost = React.createClass({

  calculateCost: function(){
    totalCost = 0;

    this.props.projects.map(function(project){
      cost = project.project_cost == null || project.project_cost == undefined ? 0 : parseFloat(project.project_cost)
      totalCost += cost;
    })

    return totalCost.toFixed(2);
  },

  render: function() {

    return(
      <div style={{marginTop: "30px"}}>
        <h4>
          Total Project Cost
        </h4>
        <h4>
          {this.calculateCost()}
        </h4>
      </div>
    );

  }

});

Here is where React really starts to shine. Since our components were already hooked up to ProjectsContainer, adding ProjectCost was quite simple. Without React, we would need to remember to update the total project in each of our ajax calls, but only when projects are successfully added or updated. This is inconvenient and makes our code starts to spread out in an unmaintainable way.

In retrospect for this simple application React was probably overkill. The dynamic features implmented here could probably have been accomplished utilizing AJAX and other exising Rails tools. Although even for this relatively small example, I suspect the code would already start to become a bit difficult to follow. In a future post we’ll compare the app as implemented here with React to the same app built using just ajax and the Rails jquery_ujs helpers.

Hopefully this post has given you a better idea of why, when and how to use React as a view library alongside Rails.

You can get the code for the finished project here

Web Application Startup Guide

A 30-page ebook that covers positioning, marketing, pricing, and building your startup product, plus more.