Rails 7 React/Redux Development with esbuild

Published: May 23, 2023

Rails 7 provides a couple of approaches to bundle a rich JavaScript application such as SPA. To create the JavaScript application, we should specify j|--javascript option with importmap (default), webpack, esbuild or rollup when rails new command gets run. Although webpack is still among the choices, it has been retired as describe in the https://github.com/rails/webpacker/blob/master/README.md. The choice here is esbuild since it is friendly to JavaScript development, for example, starting from yarn create react-app .... The esbuild is gaining popularity and known to run very fast with its Go-lang implementation.

This blog post creates React/Redux application on top of Rails 7. The application is a sample counter app which comes from what yarn create react-app [app name] --template redux-typescript command creates.

Create a Rails App with esbuild Option

The command to create an app is something like:

% rails new [APP NAME] -j esbuild -T

The -j esbuild option installs frontend development packages/tools. Additionally, the command, ./bin/rails javascript:install:esbuild, gets run during the app creation. The package.json, Procfile.dev and couple other files for JavaScript development are also created.

Create an Entry Point for ReactJS App

The next step is to create and entry point for ReactJS app. All incoming HTTP requests are received by controllers on Rails. Following such Rails style, the entry point to ReactJS app is also a controller. However, instead of rails g controller ..., stimulus generator is used for this. The generated controller is a JavaScript class, which is a subclass of stimulus Controller.

% rails g stimulus react

Above generates app/javascript/controllers/react_controller.js and updates app/javascript/controllers/index.js. The generated controller class is equivalent to ReactJS app’s index.tsx(jsx). What we write in index.tsx should go to a connect method in the generated controller class.

Create a View to Mount ReactJS App

If the ReactJS app is created by yarn create react-app ... or npm, npx command, the app has a mount point in public/index.html, something like: <div id="root"></div>. It is Rails, so we should create a controller.

% rails g controller pages home

Above creates a couple of files as we know. Edit app/views/pages/home.html.erb and add the mount point.

<%# app/views/pages/home.html.erb %>

<h1>Pages#home</h1>
<p>Find me in app/views/pages/home.html.erb</p>
<%= content_tag(:div, "", id:"root", data:{ controller: "react" }) %>

Also, edit config/routes.rb to add a path to pages#home.

# config/routes.rb

Rails.application.routes.draw do
  root 'pages#home'
end

Setup Basic React TypeScript App

At this moment, the Rails side is ready. However, the JavaScript side has a package.json file only, which is like right after yarn --init ran. Since it is a React TypeScript app, install basic packages.

% yarn add react react-dom @types/react @types/react-dom typescript

Also, TypeScript initialization should be done.

% tsc --init --project tsconfig.json --noEmit --jsx react-jsx

Redux Toolkit Counter Example

When the redux-typescript template is used to create a ReactJS App, the counter example comes with it. For examples, yarn create react-app my-app --template redux-typescript command creates files below (excludes node_modules directory):

.
├── README.md
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── app
│   │   ├── hooks.ts
│   │   └── store.ts
│   ├── features
│   │   └── counter
│   │       ├── Counter.module.css
│   │       ├── Counter.tsx
│   │       ├── counterAPI.ts
│   │       ├── counterSlice.spec.ts
│   │       └── counterSlice.ts
│   ├── index.css
│   ├── index.tsx
│   ├── logo.svg
│   ├── react-app-env.d.ts
│   ├── reportWebVitals.ts
│   └── setupTests.ts
├── tsconfig.json
└── yarn.lock

We want files under src directory. How to map those files under app/javascript might be controversy. Some might create a components directory. However, as for Redux Toolkit, features and/or app directories are more common.

The app here is created by copying files under src to app/javascript almost as those are.

app/javascript
├── App.tsx
├── app
│   ├── hooks.ts
│   └── store.ts
├── application.js
├── controllers
│   ├── application.js
│   ├── hello_controller.js
│   ├── index.js
│   └── react_controller.js
├── features
│   └── counter
│       ├── Counter.module.css
│       ├── Counter.tsx
│       ├── counterAPI.ts
│       ├── counterSlice.spec.ts
│       └── counterSlice.ts
└── logo.svg

Handling of .css files will be mentioned later since that needs a bit of fix.

To run the counter app, Redux Toolkit and react binding packages should be installed.

% yarn add @reduxjs/toolkit react-redux

Update react_controller.js

Previously mentioned, app/javascript/controllers/react_controller.js is equivalent to ReactJS app’s index.tsx.

The file looks like below to run the counter app.

// app/javascript/controllers/react_controller.js

import { Controller } from "@hotwired/stimulus"
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from "react-redux";
import App from '../App';
import { store } from '../app/store';

// Connects to data-controller="react"
export default class extends Controller {
  connect() {
    const container = document.getElementById('root');
    const root = createRoot(container);

    root.render(
      <React.StrictMode>
        <Provider store={store}>
          <App />
        </Provider>
      </React.StrictMode>
    );
  }
}

Update package.json scripts section

When the Rails app is created, package.json’s scripts section looks like below.

"scripts": {
  "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets"
}

As in the above directory tree, the counter app has .tsx and .svg files under app/javascript. So that esbuild can load those, two loaders should be added to the esbuild option.

Additionally, the script section should have TypeScript check.

After the update, the script section looks like below:

"scripts": {
  "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets --loader:.js=jsx --loader:.svg=file",
  "check-types": "tsc --project tsconfig.json --noEmit --watch --preserveWatchOutput"
}

Avoid Sprockets::DoubleLinkError application.css Error

If esbuild is used in a Rails app, .css files need extra caution. We might end up in having two application.css files generated by esbuild and originally created by rails new command. If that happens, the conflict raises the Sprockets::DoubleLinkError application.css error.

When a .tsx(.jsx) file imports CSS, esbuild generates app/assets/builds/application.css. Whereas we have app/assets/stylesheets/application.css generated by rails new command. These two application.css files have the same name but different contents.

A couple to few ways would be there to avoid the error. Probably, below two are easy ones.

  1. Never ever import css files in .tsx(.jsx). Instead, write all styles in app/assets/stylesheets/application.css or take a traditional Rails way.
  2. Rename app/assets/stylesheets/application.css.

The app here mainly took the second approach, but partially the first approach. The app/assets/stylesheets/application.css was renamed to app/assets/stylesheets/application-rails.css. The app/views/layouts/application.html.erb file got one more stylesheet_link_tag shown below:

<!DOCTYPE html>
<html>
  <head>
    <title>React/Redux App</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= favicon_link_tag 'favicon.ico' %>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= stylesheet_link_tag "application-rails", "data-turbo-track": "reload" %>
    <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
  </head>

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

All styles in index.css and App.css are moved to app/assets/stylesheets/application-rails.css. The css import was removed from App.tsx and app/javascript/controllers/react_controller.js. However, app/javascript/features/counter/Counter.module.css is there, which is imported in app/javascript/features/counter/Counter.tsx.

Use bin/dev, not rails s

To run the Rails app, use bin/dev. As defined in Procfile.dev, we need Rails server and esbuild with watch option. The bin/dev command does that. If everything goes well, the counter app below shows up at http://localhost:3000/ .

img: react redux counter app

Code

The example Rails app code is on the GitHub repo. Please see https://github.com/yokolet/rails7-typescript-redux-counter-example

Latest Posts

Real-time App on Rails by Action Cable

The previous blog post, WebSocket on Rails by Action Cable, focused on WebSocket as a protocol. As in the previous post, by default, Rails app responds to WebSocket connection requests without any hassle. However, other than connecting and sending ping frames, it doesn’t do anything. This blog post focuses on an application side and explains how we can create a full-duplex, bidirectional app.

WebSocket on Rails by Action Cable

In the web application domain, we hear some protocol names. Absolutely, HTTP or HTTPS is the most famous protocol that all web developers know. Although there’s a mechanism of Keep-Alive, a single request/response sequence with a single client/server is all done by HTTP. The client initiates the HTTP request to the server. Once the client receives the HTTP response from the server, communication finishes. As far as HTTP is used, the server just waits and waits. Only when the request comes in, the server can send back some data to the client. This communication style is surprisingly capable of doing many things, so most web applications are satisfied with HTTP.

Conserving Network Resources -- Keep-Alive

“When you type a URL in your browser, what will happen?” If you are a web developer, you might have answered this sort of interview question once or twice. It would be a popular question to test the knowledge how the Internet works. As it is famous, you will find many answers here and there online.