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

Vite + Vue + Bun on Rails

Vue.js is one of frontend frameworks gaining popularity among rapidly emerging JavaScript technologies. The combination of Vue.js and Rails is becoming more popular as well, however, Vue.js development on Rails is not so straightforward. The reason would be that Vue.js relies on Vite for a development environment such as HMR (Hot Module Replacement) and bundling.

Bun + React on Rails

In the frontend world, new technologies keep emerging rapidly these years. Still, React is a well-established and very popular frontend framework, Vue.js, Svelte, Astro and more frameworks are gaining popularity. Not just the frameworks, tools for a transpiler/bundler or sort are also under a rapid development. In JavaScript domain, esbuild, rollup, vite and some more are out.

Ruby on Rails Secrets Management

A web application needs various kinds of values, params and etc which should not be revealed to the public, say GitHub repo. For example, API keys, tokens, passwords, and endpoints, all those should be kept secret. Another important factor is that such secrets should be shared among the team members. Additionally, all those secrets should come along with the deployment.