Part 1: Getting Started with Rails ❤️ React: Options A, B, and C

Intermediate

Rails and React go together like peas and carrots.

Rails, on the backend, for the agility of ActiveRecord and the rest of the web stack eco-system, and React, on the frontend, for the awesome power that the shadow DOM can give to highly interactive performance-driven experiences.

In the earlier days of React+Rais, the first solutions were built around the asset pipeline paradigm, which was the dominant way to deploy Javascript for Rails 4.2, 5.0, 5.1.

In this series, I will cover the basic paradigm of building both a Rails application as a backend and React application as a frontend.

In this post I’ll explore in depth three separate paradigms of architecture:

Option A: Separated AppsOption B: Mix & MingleOption C: React-In-Rails With Separation
Separate apps on separate domainsReact-in-Rails, no separationReact-in-Rails, separate ‘apps’ that operate within one deployment. Build your React app inside of Rails, but don’t let Rails act as anything other than an API
Three separate paradigms of React?Rails apps

Option A

Option A means writing two separate apps: One, a React app deployed on something super simple, like express. Two, a Rails app deployed on a 12-factor deployment platform. You are recommended to keep both apps in the same Git repository (this is called a monorepo, explained below).

Unfortunately, a monorepo can’t use the Heroku pipeline, and requires special considerations for a two-app deployment.

However, you can use a Cypress test suite to do in-repository end-to-end testing. This exciting (relatively) new option brings React into the world of end-to-end testing frameworks that can be successfully deploy with a Rails backend.

The downsides to Option A still exist (separate deployments, can’t use the Heroku Pipeline), so the other options (combining your Rails + React app into one app) are explored below.

Option B
Gems like rails-react and ReactOnRails bridged the gap between what Rails has out of the box and getting your React code built within your Rails repo. These gems still offer a bit of functionality, so I cover the basics of that approach in “Option B.” With Rails 6.0, webpacker is now a first-class citizen in Rails (it comes default when you install Rails). That means it’s now fairly straightforward to wire up your React app inside of your Rails app using only webpacker.

That means you can use React inside of your Rails app and you still have two primary choices: Do I write Rails views with small bits of React components (which I will wire up directly to my UI), and use the Rails routing system, and then treat my React components as only small bits to be added to my existing Rails-based UI? (This would be Option B). Alternatively, should I go fully the React way, treating React as a Single Page App (SPA), and relying on the React routing system for all page transitions, if I have any page transitions at all? (Option C)

Option C

Option C allows us to deploy our separate, stand-alone React app inside of Rails. It’s slightly counter-intuitive, but it means we can be purists about writing a Single Page App and also treat Rails as an API-only backend. Frankly, it’s the best of both worlds but isn’t good for SEO.

I’ll also show you how easy it is to set up React within Rails (“Option B” or “Option C”) and introduce a testing model that features the best of both Ruby testing (Rspec with Capybara) and simple Javascript testing (Jest and Enzyme).

In part 1 of this post (what you’re reading now), I’ll write some failing specs and review the concepts of measuring code coverage using Jest’s built-in coverage tool and simplecov on the Ruby side.

In Part 2 of this post, we’ll implement a very simple app to make our specs pass and begin to talk about some implementation choices that most Rails-React apps will face.

API or Single Domain?

Option A: Separated AppsOption B: Mix & MingleOption C: React-In-Rails With Separation
Separate apps on separate domainsReact-in-Rails, no separationReact-in-Rails, separate ‘apps’ that operate within one deployment
Must be separate subdomains Same domain Can be same domain, or optionally different domains

One of the first questions— and really it is a question of organizational structure— is this: Will you develop a React app separated from your Rails app entirely. In other words, on it’s own domain and hosting architecture.

Option A: One app is Rails and the other app is Express JS (or Node JS) running your React application. That means you need to keep two applications in the cloud. In this case, your frontend is your frontend and your backend is your backend and never the twain shall meet. You will probably even keep them in separate repositories in Git.

For Option B, you will probably be using something like Server-side rendering, so treating your “frontend” distinct from your API-based “backend” doesn’t make sense conceptually. For this reason, you’ll probably be on the same domain.

With Option C, you get the most flexibility: You can choose to run them at the same domain (example.com) or at at public-facing domain and an API-domain (example.com and api.example.com).

Just keep in mind that in Rails you must now explicitly tell Rails what domains to respond to, which you do in config/environments/production.r by adding to the config.hosts array (this tells Rails what hostname it should respond to):

Rails.application.configure do
  // .. more configs
  config.hosts << "example.com" 
  config.hosts << "api.example.com"

If you’re going to have your Option C (same container, separated ‘apps) respond at different subdomains (which is a good forward-planning strategy thinking if you ever want to switch from Option C to Option A). This isn’t something you normally need to think a lot about— this is just to demonstrate that if you choose Option C, you can still reasonably have the two apps work on the same architecture but at different domains today, while still having the forward-planning option of splitting up your two apps into separate apps (moving to Option A) in the future and having them still work at different domains. (To keep yourself from being locked-in to the same domain, I’d recommend this as a best practice.)

Remember, if your frontend talks to your backend at the same subdomain, you don’t have to worry about CORS settings, which is an advantage because it means less things to worry about.

Server-Side Rendering

Option A: Separated AppsOption B: Mix & MingleOption C: React-In-Rails With Separation
Server Side Rendering

Pros & Cons for the Options

If your company or team 1) has Rails developers and React developers (who are different people), and 2) they want to keep their domains of expertise separated, then Option A is a good choice for you. It will require “twice the price” in terms of your infrastructure costs (brain space and real server time), but that should be negligible to the benefit you get out of Option A if it makes the development team more efficient. In the Separate Apps (Option A) strategy, generally, you think of the communication between frontend and backend as a “contract” between two parts of the system— in literal terms, the JSON parameters passed in and the JSON parameters returned by the API.

Before Rails 6, you could host React on Rails if you either 1) bundled it with the asset pipeline, or 2) installed webpacker yourself. As of Rails 6, you can easily and quickly package React inside Rails using webpacker.

Around the early days of React, it was popular to mix & mingle your React application inside of a Rails framework. Gems like react-rails and react_on_rails promote this approach.

PROS:


CONS:

• You have separate apps.

• You have to mess around with CORS

• No server-side rendering in Rails, and treats the Rails app as an API.

You will like Option A if you have separate teams working on a frontend and backend app, want tp have separate code repositories for your two separate apps, and/or don’t want to be locked into Rails as a backend or are only experimenting with Rails as a backend among other options.

Option B: Mix & Mingle.

Option B is what a lot of Rails developers did in a few years after 2015 when learning React: they fit their existing knowledge & framework of how Rails works and wrapped it around how React works. You will love Option B if you don’t like the idea of rendering everything only in the front end and treating the Rails only as an API. You will also like Option B if you are building something you want to be crawlable by Google, as it is more inherently SEO-friendly.

The 3rd option (Option C)— might I call it a “middle path”— is ironic: We will package React inside of our Rails, but we then will not allow the Rails code to act as anything more than an API. In other words, we treat the “separation of concerns” (vis-a-vis parts of the system) as strict: The frontend is the frontend and the backend is the backend, we just happen to package it all up using Rails.

PROS:

• Keep your React app entirely within your Rails app (repo, domain, etc).

• Don’t have to mess around with CORS

• Code splitting & server-side renering (good for SEO)

CONS:

• Lock-in yourself to Rails.

You will like Option B if most of your app does fine in Rails, you like Rails and want to stay in Rails, and you want some small elements to use React in isolated areas.

Option C: Isolated React Inside Rails with Rails-API

Because there remains no good way to host a Rails app on a Node.JS server as far as I know. (Theoretically, one could run both Puma and Node on the same machine and have some kind of middleware to route requests correctly to two different apps running on the same architecture. But this goes against 12-factor deployment philosophy and is so not compatible with modern deployment platforms like Heroku, Render, and Netlify.)

PROS:

• Don’t have to mess around with CORS

• Keep your React app entirely within your Rails app (repo, domain, etc).

CONS:

If you are trying to integrate legacy systems or legacy apps, this might be difficult. (For example, the strategies here will work for Rails 6.0 on webpacker)

• No SEO out of the box (full client-side rendering)

• No SSR (server side rendering)

You will like Option C if you want to delegate full navigational control to React, and you’re ready to implement the React Router or another router for your page transition. You can also use this option to keep your React app deployed inside of your Rails app but not couple your React code too tightly to your backend (Rails) implementation, in case you want to move off Rails in the future.

Planning for the Future & Cost of Change

Option A: Separated AppsOption B: Mix & MingleOption C: React-In-Rails With Separation
No lock-in: Rails can be fully switched out for another backendLock-in with Rails: The more code you write the more costly it will be to switch off RailsLow-cost to deploy; enforced separation between apps
Cost of lock-in for Ruby on Rails

Maintenance & Deployment

Option A: Separated AppsOption B: Mix & MingleOption C: React-In-Rails With Separation
Two separate deploys: CostlyOne deploy (easy)One deploy (easy)
Can’t use Heroku Pipeline Can use Heroku PIpeline Can use Heroku PIpeline

Options B and Option C let you integrate with Heroku Pipelines, which generate a unique URL for each

More considerations

Page Routing

React Router

Turbo Rails (or Turbolinks before Rails 6)

React Router

Option A with a Monorepo: Use Cypress For Testing

What is a monorepo?

A monorepo means that you put your Rails API code (or backend code) together with your React code in the same repository.

It looks like this:

Do I have to use a monorepo?

No, but you are strongly recommended to.

What are the benefits of a monorepo?
Integrated code changes can be tested using test automation tools. If you have separate repos, then one feature request could require the management of branches in two separate repos, thus causing headaches and extra intervention.

Are there times when I might not want to use a monorepo?

Most small apps, startups, and entrepreneurial setups are best suited for a monorepo. However, teams that are extremely large or have little dependencies between the backend or frontend can reasonably consider separating their repositories. However, this, unfortunately, means that you lose the ability to in-repository testing on your feature branches, which is a huge benefit to having a monorepo so this decision should be made carefully.


Option A, Concern #1: How to Create a Monorepo (Rails-React)

Both Rails and create-react-app initialize separate Git repos, and unless you want to use a subrepo (which I don’t recommend), you have to go through these steps

Create the Rails API app using:

rails new MyGreatAppAPI

Then remove the Git repo that the Rails tool by first changing into the new Rails app directory, then fully removing the invisible .git directory that was created by Rails.

cd MyGreatAppAPI/
rm -rf .git/

Go back up one directory

cd ../

Now create the React app using

npx create-react-app my-great-app-ui

Again, repeat the steps to remove the Git repo created by the create-react-app tool

cd my-great-app-ui/
rm -rf .git/

And finally go back up to the root directory before initializing Git yourself on the monorepo

cd ../
git init

Now you will have a single repository with no sub-repositories for you Rails or React app. That’s what makes it a monorepo.

Option A, Concern #2: CORS (from the fetch side)

In a standard Rails install, CORS or “Cross origin request system,” is what makes sure that the origin domain (that is, the domain making the request, or the client), matches the target domain, or in our case the server.

We actually will encounter two problems, which we’re going to solve the wrong way first. (By disabling CORS)

From the Javascript code, if we don’t have CORS set up correctly, we’ll see:

Even if we do tell Javascript to allow the cross-origin request, Rails won’t respond correctly to the incorrect client.

To fix Rails, we’ll install the rack-cors gem, which is very easy:

Add to your Gemfile

gem 'rack-cors'

Then create a file at config/initializers/cors.rb

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '127.0.0.1:3000' # for development
    resource '*', headers: :any, methods: [:get, :post, :patch, :put]
  end
end

Here, the fetch mech