Setting up Rails 7 for TypeScript and React

Rails 7 has been released recently, introducing quite a significant front-end mechanics revamp. Webpacker has been retired, and DHH now makes many compelling arguments in favor of a no-Node approach.

Well, maybe it'll get us somewhere someday, but at the moment, almost all of us prefer our TypeScript and/or JSX compiled to continue delivering quality stuff.

If you're not ready to embark on this delightful "no-transpiling" journey quite yet, there's a plan B.

In this article, we'll discuss how to set up Rails 7 for TypeScript and React, and we'll use the magnificent esbuild builder for that (Yay!).

Installation

The good news is that esbuild does all the heavy lifting for us. TypeScript and JSX are supported out of the box. All we need to do is employ the jsbundling-rails gem.

We can install it along with a new rails app (using the -j esbuild flag), or if upgrading:

  1. By adding jsbundling-rails gem in Gemfile and running bundle install
  2. And then running ./bin/rails javascript:install:esbuild

The last command will install esbuild, and add this command into package.json:

// package.json
"scripts": {
  "build": "esbuild app/javascript/application.tsx --bundle --sourcemap --outdir=app/assets/builds"
}

It's very straightforward. It takes the files from the app/javascript directory and then compiles them into app/assets/builds where they get picked up by the asset pipeline.

There's also the Procfile.dev file, which is used to run several commands at once (using foreman). Just remember to start your app with ./bin/dev instead of the ./bin/rails server. It will run both rails server and the esbuild.

Now rename application.js into application.tsx, put some JSX into it, and you're done.

// app/javascript/entrypoint.tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'

const App = () => {
  return (<div>Hello, Rails 7!</div>)
}

document.addEventListener('DOMContentLoaded', () => {
  const rootEl = document.getElementById('app')
  ReactDOM.render(<AppWithState />, rootEl)
})

Include the file into the view with javascript_include_tag 'application' (no more javascript_pack_tag!).

Super-simple, and it's also super fast thanks to esbuild.

Getting TypeScript errors

esbuild compiling is unbelievably fast, but it doesn't do any type-checking. We can still run it separately from the command line with something along these lines:

tsc --project tsconfig.json --noEmit

I prefer adding that into project.json script and into the Procfile.dev file.

// package.json
"scripts": {
  "build": "esbuild app/javascript/application.tsx --bundle --sourcemap --outdir=app/assets/builds"
  "check-types": "tsc --project tsconfig.json --noEmit --watch --preserveWatchOutput"
}

Note the last flag --preserveWatchOutput. It prevents tsc from automatically clearing the screen and blowing up the whole foreman output.

Then add it to the Procfile.dev:

types: yarn ts-check

Next steps

This setup is what works for me at the moment. I imagine, in the meantime, more tools and gems emerge, providing better Rails integration, but so far, I'm pretty pleased with the ability to use esbuild.

This setup is what works for me right now. I imagine, in the meantime more tools and gems emerge providing better Rails intergration, but so far I'm pretty pleased with ability to use esbuild.

  1. Read this blog post for live-reloading.

  2. Rails 7 introduction

  3. A visionary post by DHH