How to use React with Rails 7

 
Rails 7 is coming and it has changed the way it deals with JavaScript (again).

Compared to using React with Rails 6, for example, the options have changed a bit.

We'll look at 4 ways you can use React with a Rails 7 app:

1. Import maps (the new Rails 7 default)

2. Using a JavaScript bundler (esbuild, rollup.js, Webpack)
    a. Without webpacker (new Rails 7 default)
    b. With webpacker via gems like react-rails and react_on_rails

3. A separate frontend app that talks to a Rails API.

Before we dive into each method, let's first install Ruby 3 and Rails 7.
You can install and use Ruby 3.0.2 (latest stable version as of the writing of this tutorial) using Ruby version manager (rvm) like this:
$ rvm install 3.0.2
$ rvm use 3.0.2

You can install Rails 7.0.0.alpha2 like this:
$ gem install rails -v '7.0.0.alpha2'


Let's create a new Rails 7 app:

$ rails _7.0.0.alpha2_ new eventlite


As you will see from the output of that command, Rails no longer installs any node packages by default. There's no yarn, no package.json, no Webpack or webpacker!
Everything is setup via gems and installation scripts for import maps and hotwire.

1. Import maps


What are import maps? Let's hear it straight from the horse's mouth (the Github repo for importmap-rails):
Import maps let you import JavaScript modules using logical names that map to versioned/digested files – directly from the browser. So you can build modern JavaScript applications using JavaScript libraries made for ESM without the need for transpiling or bundling. This frees you from needing Webpack, Yarn, npm, or any other part of the JavaScript toolchain. All you need is the asset pipeline that's already included in Rails.


And what's the benefit of using import maps?

With this approach you'll ship many small JavaScript files instead of one big JavaScript file. Thanks to HTTP/2 that no longer carries a material performance penalty during the initial transport, and in fact offers substantial benefits over the long run due to better caching dynamics. Whereas before any change to any JavaScript file included in your big bundle would invalidate the cache for the the whole bundle, now only the cache for that single file is invalidated.

Rails 7 creates a new config file called importmap.rb where you can define which JS libraries you want to import by "pinning" them.

Here's the default configuration:

# Pin npm packages by running ./bin/importmap

pin "application", preload: true
pin "@hotwired/turbo-rails", to: "turbo.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers"

We can pin more libraries by running the pin command. Let's add react and react-dom:

$ ./bin/importmap pin react react-dom

Running that adds the following lines to importmap.rb:

pin "react", to: "https://ga.jspm.io/npm:[email protected]/index.js"
pin "react-dom", to: "https://ga.jspm.io/npm:[email protected]/index.js"
pin "object-assign", to: "https://ga.jspm.io/npm:[email protected]/index.js"
pin "scheduler", to: "https://ga.jspm.io/npm:[email protected]/index.js"

Let's create a directory called "components" under app/javascript for storing our React component code.

Then we can pin all the code from that directory, so any components we create will be added to the import map:

pin_all_from "app/javascript/components", under: "components"

We specify a short name "components" by passing a value for under, so that we can import it in app/javascipt/application.js without having to write the full path:

import "components/hello_react"


Now, let's say we want to render a React component inside a div on a page in our app.

Let's add a div with id "app" to the page:

<div id="app"></div>
Then let's create a React component called Hello in a file called "hello_react.js" under the components directory:

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

const Hello = props => (
  React.createElement('div', null, `Hello ${props.name}`)
)

Hello.defaultProps = {
  name: 'David'
}

document.addEventListener('DOMContentLoaded', () => {
  ReactDOM.render(
    React.createElement(Hello, {name: 'Rails 7'}, null),
    document.getElementById('app'),
  )
})

As you can see from the code above, we need to use React.createElement because we can't write JSX.

The biggest benefit of the import maps method is there's no transpilation or compilation step.

But for React, that means we can't use JSX.

And for this reason, I don't recommend this method for using React in a Rails app.

Writing React without JSX is like building web apps in assembly language. It's not practical.

DHH showed another alternative in this demo video using the the htm library. It lets you use syntax similar to JSX (with JS tagged templates), but it's still not JSX.

So, I don't recommend it.
Next, we'll look at using React with Rails using a JS bundler like esbuild or webpack (without webpacker).