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?
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.
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.
!Sign up for a free Codeship Account
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 fordocumentation_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 aslocation
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 thesubmitClaim
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.