React in Ruby on Rails without webpacker

Petr Marek
Aug 31, 2018 · 5 min read
Photo by Aleksandr Saenko on Unsplash

There’s plenty of options when it comes to integrating React into a Rails app. All of the gems use webpacker. I have a deep distrust of having node & npm installed on the server and offer a webpacker-free alternative. Not forcing webpacker, backend devs don’t need to hassle with setting and running anything from the node.js world.


We’ll create a sample Rails project which will have a standalone React application in a subfolder. Rails will contain a built version of the React app, meaning that React and everything-node is only needed when changing the React code. To switch between the dist version and live react server when developing locally, we’ll use an ENV variable.

Rails & React setup

Start by creating a sample rails project.

$ rails new react-rails --no-webpacker
$ cd react-rails
$ rails server
On http://localhost:3000/ you’ll be greeted by the default Rails homepage.

Next, let’s use Facebook’s create-react-app to initialize a frontend subfolder, which will contain everything-reacty. Note that you can create the React app any way you prefer, create-react-app is used for the sake of simplicity.

$ npx create-react-app frontend

If using create-react-app, we may want to change the default port of the server from 3000 to something else, so that Rails can run on 3000. Add an .env file setting the port

$ echo 'PORT=3001' > frontend/.env

Fire up the React dev server:

$ cd frontend
$ npm start
It will pop up at http://localhost:3001/. Now let’s connect it to Rails!

Connecting Rails & React

First, we generate a Home controller and delete the default index.html from public.

$ rm public/index.html
$ rails g controller home

Set up the routes:

# config/routes.rbRails.application.routes.draw do
root to: 'home#index', as: :home
end

Create the template, containing the element, that create-react-app is looking for:

# app/views/home/index.html.erb<div id="root"></div>

Upon visiting http://localhost:3000/ you’ll get a blank white page, which is perfectly correct, as the React assets are not included yet. Let’s add the React bundle to the bottom of the application layout (under the yield):

# app/views/layouts/application.html.erb<%= javascript_include_tag 'http://localhost:3001/static/js/bundle.js' %>

Refresh http://localhost:3000/ to see React connected.

Well, it’s something.

We can see that application boots, but cannot find the assets. That’s not a problem as you shouldn’t use static images from React. Server them from Rails instead.


Passing data from Rails to React

Let’s edit the React app to display some data passed from Rails. Set the data in the controller:

# app/controllers/home_controller.rbclass HomeController < ApplicationController
def index
@fruits = %w[apple banana pear]
end
end

Add it as a data attribute in the view:

# app/views/home/index.html.erb<div id="root" data-fruits="<%= @fruits.to_json %>"></div>

Pass it to the App component in the React setup script:

# frontend/src/index.jsimport React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
const rootElement = document.getElementById('root');
if (rootElement) {
const fruits = JSON.parse(rootElement.dataset.fruits);
ReactDOM.render(<App fruits={fruits} />, rootElement);
registerServiceWorker();
}

Print the fruits in the App component:

# frontend/src/App.jsimport React, { Component } from 'react';class App extends Component {
render() {
return (
<div className="App">
<h1>Fruits</h1>
<ul>
{this.props.fruits.map((fruit) => (
<li key={fruit}>{fruit}</li>
))}
</ul>
</div>
);
}
}
export default App;

On http://localhost:3000/, we get the result.

While the example is trivial, we might pass any sort of information that way — be it API urls, serialized models, constants, translations, etc. — using multiple attributes if needed.

In case we wanted to have multiple React instances on the same page, we’d change the root selector from id to class and tweak the initialize script:

const rootElements = document.querySelectorAll('.root-element');for (const rootElement of rootElements) {
const fruits = JSON.parse(rootElement.dataset.fruits);
ReactDOM.render(<App fruits={fruits} />, rootElement);
}

We might even use different components for different root elements, passing different data.

const appRootElement = document.getElementById('root');
if (appRootElement) {
const fruits = JSON.parse(appRootElement.dataset.fruits);
ReactDOM.render(<App fruits={fruits} />, appRootElement);
registerServiceWorker();
}
const nonAppRootElement = document.getElementById('not-root');
if (nonAppRootElement) {
ReactDOM.render(<NotApp />, nonAppRootElement);
registerServiceWorker();
}

Inside of the initializer, you can set up your Redux store, sagas etc. For a rather complex example, see the setup file of the React portion of sinfin/folio CMS, which enhances the administration console UX.


Building React

We can build the React app with npm run build inside of the frontend folder. That will create some files in frontend/build/static. We want to copy these files to the Rails assets folder. To do that, we can create a simple script in the frontend folder.

# frontend/bin/copy_to_rails#!/bin/bashrootDir="$( dirname "$0" )/../.."(
cd "$rootDir/frontend" || exit
cp build/static/js/main*.js ../app/assets/javascripts/frontend.js
cp build/static/css/main*.css ../app/assets/stylesheets/frontend.css
echo 'Copied build to rails assets folder.'
)

Don’t forget to add +x permissions.

$ chmod +x frontend/bin/copy_to_rails

Modify the build command in the package.json:

"build": "react-scripts build && bin/copy_to_rails"

Building with npm run build now creates frontend.js and frontend.css in the corresponding assets subfolders.


Serving the dist version

We’ll instruct sprockets to compile the assets,

# config/initializers/assets.rbRails.application.config.assets.precompile += %w(frontend.js frontend.css)

and include them in the application layout:

# app/views/layouts/application.html.erb<!DOCTYPE html>
<html>
<head>
...
<% unless ENV['REACT_DEV'] %>
<%= stylesheet_link_tag 'frontend' %>
<% end %>
...
</head>
<body>
...
<% if ENV['REACT_DEV'] %>
<%= javascript_include_tag 'http://localhost:3001/static/js/bundle.js' %>
<% else %>
<%= javascript_include_tag 'frontend' %>
<% end %>
</body>
</html>

We look at ENV to determine whether we’re willing to edit the React code. When a server is run, the built version is used by default. To develop the live version, one has to start the server with a REACT_DEV environment variable, which is as simple as:

# Don't care about react:$ rails server# Develop react:$ REACT_DEV=1 rails server

I’ve created an example github repository containing the code mentioned throughout the article.