Ruby

Integrating React in a 6+ Year Old Rails App

When it comes to modern JavaScript frameworks, it’s tempting to think of them as belonging to the bleeding edge as far as web technologies. But it can be a momentous task to install a modern framework into an older application — that can actually be the downfall of using these frameworks.

Last year, we were approached by one of our existing partners for a current Rails app project with a complex user interface and workflow. The application was over six years old. While it was certainly an option to build out this project using jQuery, we knew that we would reap the benefits of ease of maintainability and development speed by introducing a modern JavaScript framework.

Enter React

After a few weeks of research, we came to the decision to use React.js. We made this decision mainly based on the ease of installing React into a Rails applications via the react-rails gem.

I’ve been really impressed with this gem during this project. Not only does it allow you to place React components into a Rails html.erb view, but it will convert all of your ES6 JavaScript syntax into ES5 (via a dependency, Babel), so that your React components will render correctly cross-browser.

It was great for our developers to be able to use some of this new syntax. It increases overall developer quality of life and ease of code legibility improvements. To have that ability in an app built in 2011 is really exciting and is going to make the application more valuable throughout its lifetime.

Relying on Rails Routes

Our goal with React was to make sure that it was as “stupid as possible.” By this, I mean that all data and errors were fed to React via Rails. React handles manipulating data via JavaScript objects stored in React’s state and then passes it back to Rails to update/create records and respond to errors.

We made the decision to reduce frontend complexity by allowing Rails to manage the routes and allow for traditional HTTP requests in between pages. I absolutely recommend this option if an application does not require single page application functionality.

Why?

  1. It works beautifully with Rails. Being able to define separate controller actions with different instance variables that get passed down into the react components made things super simple.
  2. Because we could rely on various controllers and controller actions, we could write controller tests to easily check the data was defined in each of these GET routes.

Serializing Data

Another big win for us was using ActiveModel::Serializer to customize how we sent data for different resources. This became really useful as time went on to be able to contain some logic about how certain objects should look as data transitions from Rails to React.

Let’s walk through an example of our documenting claim page.

Transitioning data from Rails to React

In this example, our resource is called Claim; it stores the data for a user’s insurance claim.

We will be using one GET route for our claims controller, like so, while the others (new, create, update, destroy) are provided by the call to resources :claims. It was necessary to define these unique named routes because this feature is a multi-step process that needed more than just the CRUD routes provided on resources :claims.

In reality, we had more routes listed here. For clarity’s sake, I’m just going to focus on documenting our claim.

resources :claims do
   member do
     get :documenting_claim
     patch :documentation_submitted
   end
end

This also means that we can define our data separately for each controller action if necessary. We can also use Rails routes to route to any step in the process we need to.

So our controller can look something like this:

ClaimsController < ApplicationController
  respond_to :html, :json
  
  def documenting_claim
    @claim_json = ClaimSerializer.new(@claim).as_json
  end

  def documentation_submitted
    if @claim.save!
      render json: { location: documenting_claim_path(@claim) },
             status: :ok
    else
      render json: { message: 'There was an error while submitting your claim for review.' },
             status: :unprocessable_entity
    end
  end
end

Here’s a little more information about the controller:

  • We have two controller actions here, one for the GET documenting_claim request and one for the PUT/PATCH request for documentation_submitted.
  • We are using an older version of the active_model_serializers (0.9.3) gem, which means our syntax may look different than yours with regard to how you define your serialized JSON instance variable. This is because of the older version of Ruby this application is using.
  • Probably the most important thing here is how we are returning JSON with our PATCH request to this endpoint. This means that when React requests this endpoint from Rails, it will respond according to error or success.

Here’s a (very) slimmed down example of our React component and how we load it into the Rails view.

documenting_claim.html.erb

<%= react_component("DocumentingClaimContainer", claim: @claim_json) %>

documenting_claim_container.jsx

class DocumentingClaimContainer extends React.Component {
  constructor(props) {
    super(props);
    // this.props.claim is passed from Rails serializer
    this.state = { claim: this.props.claim }
    this.submitClaim = this.submitClaim.bind(this);
  }

  submitClaim() {
    $.ajax({
      url: `/claims/${this.state.claim.id}/documentation_submitted`,
      headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') },
      dataType: 'json',
      type: 'PATCH',
      data: this.state.claim,
      success: (data) => { window.location = data.location; },
      error: (response) => {
        this.setState({ error: response.responseJSON.message });
      }
    });
  }

  render() {
    <DocumentingClaimForm submitClaim={this.submitClaim}
                          claim={this.state.claim}
                          error={this.state.error} />  
  }
}

Here’s a bit more context about the React components in this example.

  • On a successful Ajax request, we use window.location to physically change the location of what route is defined as location in the request.
  • On error, we set whatever error text that will show up in the component that will be nested in DocumentingClaimForm.
  • We would use the DocumentingClaimForm component to render all the markup here and be in charge of having the button that would trigger the submitClaim method. If you need more info about this, you can read plenty about it in any articles about React container and presentational component organization (like this one).

The following is an example of what a serializer might look like for a resource. This is what would be called every time you hit the GET route.

claim_serializer.rb

class ClaimSerializer < ActiveModel::Serializer
  attributes :id, :status, :terms_and_conditions
  
  def terms_and_conditions
    if object.company.terms_and_conditions
      object.company.terms_and_conditions.file.url
    else
      '/assets/Default_Terms_And_Conditions.pdf'
    end  
  end
end

The great thing about serializers here is you can use public methods from Claim, attributes from the database or, like we are doing here with terms_and_conditions, define your own methods to only be used by React.

In this case, this would display the URL of the carrierwave file, if it existed, but display a static asset otherwise.

Another important tactic we had was to pass these serialized objects back into React from our Rails controller endpoints via JSON. A good example of that would be updating a nested resource, like ClaimItem on Claim for example:

claim_items_controller.rb

def update
  if @claim_item.update(claim_item_params)
    render json: { message: 'Claim Item Successfully Updated!',
                   claim_item: ClaimItemSerializer.new(@claim_item),
                   claim: ClaimSerializer.new(@claim_item.claim) }
  else
    render json: { message: @claim_item.errors.full_messages.join(', ') },
                   status: :unprocessable_entity
  end
End

In your response to update on ClaimItems, we could now update the state of both Claim and ClaimItems to reflect how the data was represented in Rails.

Wish List for the Future

There were really very few pain points using React this way in a relatively complex application with over six years of history and changes. But still, there are a few things I wish we could have used on this project.

One of the biggest things that we do miss out on by using react-rails (and Rails 5) is not being able to use webpack. This is the biggest nice-to-have when you can use React in an updated application (this project was Rails 4). Webpack is going to make your life easier by allowing you to only load the components that are needed on the page, rather than loading all of your JavaScript as one big chunk every time you make a request for a page.

We didn’t find that a whole lot of JavaScript libraries were necessary for this project, but if we wanted to include popular React libraries like Redux, it would have been much easier to install using modern JavaScript tools surrounding the webpack and NPM communities.

If you are interested in more of a webpack approach, I would suggest the react_on_rails gem over react-rails.

Overall, I was impressed by the improvements we were able to make to this existing Rails application by choose to work with React.js. I’m really excited about React as a whole, and am looking forward to implementing it in more new and existing projects in the future.

Published on Web Code Geeks with permission by Corinne Kunze, partner at our WCG program. See the original article here: Integrating React in a 6+ Year Old Rails App

Opinions expressed by Web Code Geeks contributors are their own.

Corinne Kunze

Corinne Kunze is a full stack web developer with a design background. She specializes in building JavaScript and Ruby on Rails applications. She currently creates and rebuilds apps at Planet Argon.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button