Part 3: Option B or C Rails ❤️ React Quick Setup

Intermediate

The section is for those who have chosen Option B or Option C for building your React app inside of your Rails app.

This requires a monorepo.

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
Use Cypress for testing and SKIP TO PART 3Want to use Cabypara?
This lesson is for you.

Want to use Cypress?
Want to use Cabypara?
This lesson is for you.

Want to use Cypress?
Three separate paradigms of React-Rails apps

In this tutorial, we will be using Capybara. If you prefer to use Cypress, please skip this lesson and see Option A With a Monorepo: Use Cypress For Testing.

If you are using Cyrpess (as covered in Part 1) or have an Option A (split apps in a monorepo) setup, continue to Part 3.

Step 1: Create a New Rails app

rails new MyGreatApp --database=postgresql

Change directory into the app and: before continuing, I suggest you git init your repo and commit your changes as “initial commit.” The reason I suggest you do this is that in the next step, you’ll run a command that will make a few configuration changes you should look at. Looking at your changes step-by-step is easy when you commit along the way and will help you understand better what you’re doing.

Then, make your database with

rails db:create && rails db:migrate

COMMIT YOUR CHANGES HERE BEFORE CONTINUING.

RAILS 7 ONLY

Add to your Gemfile

gem 'webpacker'

RAILS 6.1

You already have this gem by default, skip to Step 2

Step 2: Use Webpacker To Setup React Quickly

We’re going to several things right now upfront by using this

rails webpacker:install

Party!

Now run

As a result, we get a whole bunch of automatic changes, so let’s take a second to review them. If you committed before this step, you can review them with me now doing git diff. Therefore, you do not make the following bulleted modifications (you can skip to step 3) yourself— the script above has already done it for you. I am reviewing these modifications briefly to make sure you understand how webpacker has set-up your environment for using React.

•  babel.config.js

module.exports = function(api) {
  var validEnv = ['development', 'test', 'production']
  var currentEnv = api.env()
  var isDevelopmentEnv = api.env('development')
  var isProductionEnv = api.env('production')
  var isTestEnv = api.env('test')

  if (!validEnv.includes(currentEnv)) {
    throw new Error(
      'Please specify a valid `NODE_ENV` or ' +
        '`BABEL_ENV` environment variables. Valid values are "development", ' +
        '"test", and "production". Instead, received: ' +
        JSON.stringify(currentEnv) +
        '.'
    )
  }

  return {
    presets: [
      isTestEnv && [
        '@babel/preset-env',
        {
          targets: {
            node: 'current'
          }
        }
      ],
      (isProductionEnv || isDevelopmentEnv) && [
        '@babel/preset-env',
        {
          forceAllTransforms: true,
          useBuiltIns: 'entry',
          corejs: 3,
          modules: false,
          exclude: ['transform-typeof-symbol']
        }
      ]
    ].filter(Boolean),
    plugins: [
      'babel-plugin-macros',
      '@babel/plugin-syntax-dynamic-import',
      isTestEnv && 'babel-plugin-dynamic-import-node',
      '@babel/plugin-transform-destructuring',
      [
        '@babel/plugin-proposal-class-properties',
        {
          loose: true
        }
      ],
      [
        '@babel/plugin-proposal-object-rest-spread',
        {
          useBuiltIns: true
        }
      ],
      [
        '@babel/plugin-proposal-private-methods',
        {
          loose: true
        }
      ],
      [
        '@babel/plugin-proposal-private-property-in-object',
        {
          loose: true
        }
      ],
      [
        '@babel/plugin-transform-runtime',
        {
          helpers: false
        }
      ],
      [
        '@babel/plugin-transform-regenerator',
        {
          async: false
        }
      ]
    ].filter(Boolean)
  }
}

config/webpacker.yml

This is the important webpacker configuration file. Remember, webpacker will transpile our advanced ECMAScript or Typescript into cross-browser compatible ECMAScript (JavaScript)

Webpacker tells you at the top of this file:

You must restart bin/webpack-dev-server for changes to take effect

Like all Yaml files in Rails, we have a default block followed by development, test, and production settings, each in their own blocks.

This special key is at the start of every environment <<: *default

development:
<<: *default

Remember, the loader will load the settings from the current Rails environment, as defined by the RAILS_ENV environmental variable.

When doing so, this special key above tells the loader to load the settings from the default block first, and then overload them (or over-ride them) with any environment-specific settings found in the block.

In this way, you will specify the settings common to all of the environments in default and have only the fewest possible differences specified in the environments.

In here we have things like source_path, which is where we will build the Javascript from. Its default is app/javascript

public_root_path defines where the build javascript becomes available, public_output_path is where we build the packs. For Rails, this is all set up for you so you don’t have to worry about it.





static_assets_extensions defines which types of files (by file extension) will be served statically. Using them as static assets tells webpacker to always build them into the bunder. From our Javascript code, we will load and reference them using import.

The default settings come with:

static_assets_extensions:
- .jpg
- .jpeg
- .png
- .gif
- .tiff
- .ico
- .svg
- .eot
- .otf
- .ttf
- .woff
- .woff2

Config files at config/webpack/

In addition to the YAML configuration, there are now environment-dependent Javascript configs too:

These have just 3 lines of configuration, which helps webpacker define the environment. The development one looks like this:

process.env.NODE_ENV = process.env.NODE_ENV || 'development'

const environment = require('./environment')

module.exports = environment.toWebpackConfig()

As you can see, the first line defines the environment if and only if it is not already defined by process.env.NODE_ENV (which would come from the Node process). Then, the next two lines are the

package.json

If you already had Webpacker, we added these dependencies to our primary package.json file:

"@babel/preset-react": "^7.12.10",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"prop-types": "^15.7.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",

If you are starting with a new Rails 7 app, your webpacker looks like:

{
  "dependencies": {
    "@rails/webpacker": "5.4.3",
    "webpack": "^4.46.0",
    "webpack-cli": "^3.3.12"
  },
  "devDependencies": {
    "webpack-dev-server": "^3"
  }
}

Changes in yarn.lock correspond to what we see in package.json.

postcss.config.js File

Finally, we’ve now snuck in settings for using PostCSS, “A tool for transforming CSS with JavaScript.”

module.exports = {
  plugins: [
    require('postcss-import'),
    require('postcss-flexbugs-fixes'),
    require('postcss-preset-env')({
      autoprefixer: {
        flexbox: 'no-2009'
      },
      stage: 3
    })
  ]
}

Finally, take now there are now 3 directories which are in your local development but not in the Git repository. You will notice that the installer has modified your .gitignore file adding 3 directories, 2 log files, and a hidden .yarn-integrity file.

+/public/packs
+/public/packs-test
+/node_modules
+/yarn-error.log
+yarn-debug.log*
+.yarn-integrity

Remember, these files will exist for you locally but will not get checked into the repository.

COMMIT YOUR CHANGES HERE BEFORE CONTINUING.

Step 3: Install React

rails webpacker:install:react

Webpacker now supports react.js

Now we have a new round of diffs to look at. Remember, the installer has conveniently done this for you so all you need to do is pay attention to what the settings are and where they are— you don’t need to change anything now. You can see all of this just by running git diff (assuming you committed your chances as indicated above.)

babel.config.js

config/webpacker.yml

package.json

(IMPORTANT: If you are upgrading from Rails 5.2, run bundle exec rails webpacker:install before running the above command. If you are already on Rails 6, you may already have webpacker installed.)

Here we get a bunch of interesting stuff

Webpacker says nicely to us

Gives you some encouragement, no?

Step 4: Configure Webpacker to use JSX Syntax If Adding to An Older App

(No longer necessary with Webpacker 5.0 — Skip to Step 4)

If you adding to an older app, add jsx to config/webpacker.yml

- .jsx

If you missed this step, when you try to use JSX syntax inside of Webpacker, you’ll get an error like this when you try to use JSX:

Support for the experimental syntax 'jsx' isn't currently enabled

COMMIT YOUR CHANGES HERE BEFORE CONTINUING.

Step 4: Pick React Version & Choose if you want Styled Components

The examples in this series will use styled-components, a way to write your CSS alongside of your JSX components. Style-components has become widely de facto in many modern React projects so I will use it here.

yarn add styled-components

For this series I am going to show examples in React 16.9. Please note that as you work with React, you will find mind version differences often cause brittle little issues. That’s why a solid test suite is essential to writing quality React code. You are welcome to use the latest version of React, but to pick a specific version, use something like this.

If you were installing React 16.9, you’d use:

yarn add react@16.9 react-dom@16.9

or for React 17,

yarn add react@17.0.2 react-dom@17.0.2

Generally I think React and React DOM are versioned “lockstep,” which means the same numbered versions are released at the same time.

For the latest versions, see this page for React and this page for React DOM.

In package.json you should see your new React package:

“react”: “17.0.2”,

“react-dom”: “17.0.2”,

(You will also see entries in yarn.lock for the React and React DOM packages)

COMMIT YOUR CHANGES HERE BEFORE CONTINUING.

Step 5: Setup our React Environment

 mkdir app/javascript/components/

Your components will go here in this folder.

COMMIT YOUR CHANGES HERE BEFORE CONTINUING.

Step 7: Setup Cypress Testing

Step 7 Option B Strategy

In option B strategy, we’re going to leave the Rails application layout at layaouts/application.html.erb that looks like this:

<!DOCTYPE html>
<html>
<head>
<title>MyGreatApp</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>

<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>

<body>
<%= yield %>
</body>
</html>

Do nothing in this file, just notice that we are leaveing it as -is.

In your Gemfile, add

gem 'react-rails'

then run bundle install.

In views/welcome/index.erb, add

<%= react_component("my_subdirectory/HelloWorld", { greeting: "Hello from react-rails." }) %>

COMMIT YOUR CHANGES HERE BEFORE CONTINUING.

Step 7 Option C Strategy

Coverage Reports in Jest

One last little important step while we’re here configuring Jest. Coverage reports are absolutely essential to understanding what parts and how much of your code is covered

To get a coverage report from Jest, use:

yarn jest --coverage

Notice two things:

(1) You can see the code coverage report printed to the screen here.

(2) Jest has output a folder to the coverage/

RAILS ? REACT: A tool for Rubyists called simplecov also outputs content to the coverage/ folder, so we will have to deal with this name collision by modifying either Jest or simplecov to output to a different folder.

In my projects, I don’t check-in the coverage folder into source control. (Therefore, I can just delete the content after I use it.)

Go to your window system and examine what the contents of this folder.

To work with the Jest coverage report you will double-click the index.html file. It will open a web browser window where you will see:

You are now ready to go with Javascript-side testing for your React app.

I’ll explore more testing options and we’ll get to start implementing the specs and features in Part 2 of this series.

The rest of this post discusses the differences between Option B and Option C.

Mix & Mingle vs. Isolated App

Remember, Option A— having fully separated apps— is not covered in this tutorial. (This tutorial covers deploying React inside of a Rails 6 app.) Now I’ll begin to discuss Option B vs Option C and we can get to writing React components.

Option B: Mix & Mingle

Remember, these options are only for you if you choose to use the react-rails gem.

First add to your Gemfile

gem 'react-rails'

Now you must run this again to make sure that UJS is added to your application pack

rails generate react:install

Now make some boilerplate React code


rails g react:component HelloWorld greeting:string

Notice that this is what comes out:

import React from "react"
import PropTypes from "prop-types"
class HelloWorld extends React.Component {
render () {
return (
<React.Fragment>
Greeting: {this.props.greeting}
</React.Fragment>
);
}
}

HelloWorld.propTypes = {
greeting: PropTypes.string
};
export default HelloWorld

Go back to HelloWorld.test.js and import this component into the test:

import HelloWorld from './HelloWorld.js'

Now run yarn test again, and we get:

We asserted that HelloWorld returned a <div></div> that contains What is 5+2? and we have no such thing rendering from our HelloWorld div, so we get this:

Good, another failing spec. I’m going to wait to until Part 2 to implement anything there, but for the time being, let’s hook up this component to the view.

Go back to application/index.html.erb and replace “eye mate!” with

<%= react_component("HelloWorld") %>

Now, with your component wired up to your view, you should find that you can see our new component if we load the browser.

That’s exactly what our component HelloWorld does.

The first thing you should notice is that this method encourages you to create new react_component(...) calls, in Ruby, to each of your top-level components.

Under the hood, the UJS has wired up your components using some magic that you can’t quite see.

Let’s make a small tweek to our component and our specs should pass:

import React from "react"
import PropTypes from "prop-types"
class HelloWorld extends React.Component {
  render () {
    return (
      <React.Fragment>
        Greeting: {this.props.greeting}
      </React.Fragment>
      <div>What is 5+2?</div>
    );
  }
}

HelloWorld.propTypes = {
  greeting: PropTypes.string
};
export default HelloWorld

The finished result (for option B) can be seen here. Remeber, all we’ve really accomplished today is the setup of your new Rails-React app with the basic spec setup. In part 2 of this series, we’ll graduate past “hello world” and move on to some basic implementation patterns.

Option C: Isolated React-on-Rails

The third option for working with React inside of Rails is the cleanest: We’ll act as if we’re writing two separate apps (possibly with some points of touching between the two, in the parts denoted with “RAILS?REACT” emojis below.)

In reality, we simply have a React app contained within a Rails deployment and a Rails app that acts as a backend API. The integration points should be specific and focused (I recommend the ones I list here), so as to force yourself to write layers of your application in such a way that you do not tightly couple the frontend to the backend’s implementation details. You want to keep loose coupling between the frontend and the backend, but also use some coupling with smart tricks to prevent duplication of business logic between the frontend and the backend.

A few strategies for this “smart coupling,” I will call it, are shown in the “More Info” notes below.

Hook the App Globally to Your Layout

React-on-Rails with a Rails API

For this app, Rails will do no view rendering.

If you’re just getting started with a new Rails app, repeat all the instructions for Steps 1 – 5 above. (Stop when you get to the section marked “Option B: Mix & Mingle” and come back here.)

In fact, it may do just a tiny tiny bit of view rendering, right here:

Here, modify the application.html.erb file and add a single div element to your app, with an id of app. (Perhaps the most boring and basic of patterns, but it will do for this example.)

<!DOCTYPE html>
<html>
  <head>
    <title>Your App</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <div id="app">
    </div>
    <%= yield %>
  </body>
</html>

You’ll notice that I’ve put the yield below my app canvas element, but it won’t really be used much, since this app will strictly do front-end view rendering.

Make the Root React Component

Now we’ll create a file at app/javascript/my_great_app.js.

import React from "react"
import styled from 'styled-components'

const StyledApp = styled.div`
display: flex;

div {
border: solid 1px grey;
}`


const App = (props) => (
<>
Hello world!
</>
)

export default App

Setup Routes in Rails

If you skipped the section “Set up Routes and A Blank Page” above where we made a route and action on our ApplicationController, be sure to do that now.

If you start rails you should see this in your browser.

Wire Your React App to Your Rails App

Now the fun bit.

With Turbolinks:

Go to app/javascript/packs/application.js and add this code after the existing code:

import Rails from "@rails/ujs"
import Turbolinks from "turbolinks"
import * as ActiveStorage from "@rails/activestorage"
import "channels"
import QuizTakerApp from "../components/quiz_taker_app";

import React from 'react';
import ReactDOM from 'react-dom'

Rails.start()
Turbolinks.start()
ActiveStorage.start()

document.addEventListener('turbolinks:load', function() {
  ReactDOM.render(
  <App />,
  document.getElementById('app'),
  )
})

Reload your app. You should now see your component rendered correctly as Hello World!

You’ll notice the existing app component is both a stateless component and also uses that special <></> syntax which is only necessary because JSX likes to have all of the rendered component live in one root element. (You’ll see and other patterns in react and it’s called ——-).
It doesn’t need to be a stateless functional component. It can be a class, or a component implemented with the Context API (hooks). It’s implemented here as an example.

Without Turbolinks:

  • First rip out the gem from the Gemfile
gem 'turbolinks', '~> 5'

(remove the line from Gemfile and bundle install to rebuild your gems.) 

• Also remove it from package.json using yarn remove turbolinks or the cooresponding npm command. That will remove this like form package.json

"turbolinks": "^5.2.0"


• In app/javascripts/packs/application.js, remove the turbolinks line:

import Rails from "@rails/ujs"
import Turbolinks from "turbolinks"
import * as ActiveStorage from "@rails/activestorage"
import "channels"

Rails.start()
Turbolinks.start()
ActiveStorage.start()

import ReactDOM from 'react-dom'
import React from "react"
import App from "../components/app";

Assuming you did not follow the “With turbolinks” section above, you’ll want to add this code to the bottom of the file:

 ReactDOM.render(
<App />,
document.getElementById('app'),
)

If you did follow the “With turbolinks” section above and you are now removing Turbolinks, remove the event wrapper as so:

document.addEventListener("turbolinks:load",function() {
   ReactDOM.render(
    <App />,
      document.getElementById('app'),
    )
})

Be sure to replace the wrapper for the Turbolinks listener (around the ReactDOM render call) with a native Javascript listener for DOMContendLoaded:

document.addEventListener("DOMContentLoaded", function() {
  ReactDOM.render(
    <App />,
    document.getElementById('app'),
  )
}

Finally, in application.html.erb, remove the data-turbolinks-track attributes on these two lines:

<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

Once you have removed turbolinks confirm that you app loads in your development environment (browser) without any console errors.

The example app for Option C can be found here.