Configuring Ruby on Rails to Support React + TypeScript with Webpacker

This guide covers how to configure a new Ruby on Rails (RoR) application to support a React + TypeScript front-end using Webpacker.

Companion code to this post can be found on GitHub at: https://github.com/firxworx/rails-react.

For new projects, I would generally recommend against coupling a React SPA with a back-end API where each app is written in two entirely different programming languages using two completely different stacks.

However the following approach is typical and somewhat idiomatic in the Ruby on Rails ecosystem, and deployment is better supported with “white glove” hosting solutions that are popular amongst Ruby on Rails devs such as Heroku.

The approach that’s covered below can also be adapted for legacy monolithic RoR apps that need to modernize their front-end UI.

React can be introduced to a legacy app UI in phases that gradually replace parts of the UI with React components. This use-case is actually very similar to Facebook’s when they created React in the first place.

Step-by-Step Guide

Prerequisites

Ensure that you have the following prerequisites in place:

  • recent versions of ruby and node
  • yarn package manager (RoR’s preferred package manager for JavaScript)

Install rails in your ruby environment if you have not done so already:

gem install rails

The approach covered in this guide leverages webpacker, a toolset that brings webpack v5 to modern Rails projects.

Run the following command to scaffold a new rails project with support for React, replacing rails-react with your project name:

rails new rails-react --webpack=react

Add support for TypeScript:

bundle exec rails webpacker:install:typescript

The above command adds and configures dependencies that will transpile TypeScript to JavaScript using Babel via the @babel/preset-typescript package.

For more details, check out webpacker’s docs for TypeScript: https://webpacker-docs.netlify.app/docs/typescript.

editorconfig + prettierrc

If you use prettier and your editor respects .editorconfig files, you may wish to add a basic configuration.

Create an .editorconfig in your project root as follows:

root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
max_line_length = off
trim_trailing_whitespace = false

Add a .prettierrc for TypeScript with your style preferences. Here are mine:

{
  "parser": "typescript",
  "singleQuote": true,
  "trailingComma": "all",
  "semi": false,
  "bracketSpacing": true,
  "tabWidth": 2,
  "useTabs": false,
  "printWidth": 120
}

Type Checking

Type checking needs to be manually added to the Webpack compilation process. This is an important development step that ensures that code containing TypeScript-related errors will fail to build.

yarn add fork-ts-checker-webpack-plugin --dev

The plugin then needs to be added to the development environment specified by config/webpack/development.js:

process.env.NODE_ENV = process.env.NODE_ENV || 'development'
const environment = require('./environment')

const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const path = require('path')

environment.plugins.append(
  'ForkTsCheckerWebpackPlugin',
  new ForkTsCheckerWebpackPlugin({
    typescript: {
      configFile: path.resolve(__dirname, '../../tsconfig.json'),
    },
    // non-async type checking will block compilation on error
    async: false,
  }),
)

module.exports = environment.toWebpackConfig()

With TypeScript, there is no longer a significant need for the prop-types package that’s often used with React + JavaScript. Remove the package with:

yarn remove prop-types

Rename the boilerplate app/javascript/packs/hello_react.jsx to app/javascript/packs/hello_react.tsx.

Revise the hello_react.tsx file to be valid TypeScript as follows:

// Run this example by adding <%= javascript_pack_tag 'hello_react' %> to the head of your layout file

import React from 'react'
import ReactDOM from 'react-dom'
import App from '../components/App'

const Hello: React.FC<{
  name: string
}> = (props) => <div>Hello {props.name}!</div>

document.addEventListener('DOMContentLoaded', () => {
  ReactDOM.render(<Hello name="React" />, document.body.appendChild(document.createElement('div')))
})

Your editor might indicate that the some of the syntax in the above code block isn’t valid. This can be addressed by revising the TypeScript config per tsconfig.json.

My revised tsconfig.json follows. It incorporates several improvements vs. the boilerplate. You may cherry-pick individual property changes that you agree with or replace your entire tsconfig.json with my example:

{
  "compilerOptions": {
    "strict": true,
    "alwaysStrict": true,
    "strictNullChecks": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "allowJs": false,
    "declaration": false,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "esModuleInterop": true,
    "lib": ["es6", "dom"],
    "module": "es6",
    "moduleResolution": "node",
    "sourceMap": true,
    "target": "es5",
    "jsx": "react",
    "noEmit": true,
    "forceConsistentCasingInFileNames": true
  },
  "exclude": ["**/*.spec.ts", "node_modules", "vendor", "public"],
  "compileOnSave": false
}

More notable tweaks include:

  • Turning on strict mode. TypeScript isn’t TypeScript without strict mode. Period.
  • The import statements in the boilerplate hello_react.tsx require "allowSyntheticDefaultImports": true to work with TypeScript. This setting is implied by specifying the "esModuleInterop": true option which is more broad.
  • Turning on a number of options (they begin with strict* and no*) that improve code standards + quality, and help to eliminate certain classes of bugs.

Add a Controller w/ Index to Serve the React SPA

Create a controller that will serve an index page containing the React app:

rails g controller pages index

This will generate app/controllers/pages_controller.rb and scaffold an index action.

Set the root in /config/routes.rb to the new index page:

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

Start the development server:

# development server
rails s

Visit the default development URL at http://localhost:3000 to view the contents of the index page.

The boilerplate content consists of a heading and a paragraph tag (or at least it did in my case with the particular versions that I was running). This content is found in the app/views/pages/index.html.erb file. Delete the contents of this file (but not the file) and save the file.

Add the following line to app/views/layouts/application.html.erb before the closing head tag:

<%= javascript_pack_tag 'hello_react' %>

The JavaScript pack tag rigs up the hello_react script that contains the entry point to the React app.

Start working in React

For a more conventional foundation for an SPA, create the folder app/javascript/components and create App.tsx:

import React, { useState } from 'react'

const App: React.FC = () => {
  return (
    <div>
      <h1>React App UI</h1>
    </div>
  )
}

export default App

Then revise app/javascript/packs/hello_react.tsx to import and render the App component:

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

import App from '../components/App'

document.addEventListener('DOMContentLoaded', () => {
  ReactDOM.render(<App />, document.body.appendChild(document.createElement('div')))
})

References

  • https://webpacker-docs.netlify.app/docs/typescript
  • https://blog.logrocket.com/how-to-use-react-ruby-on-rails/
  • https://jtway.co/rails-typescript-react-js-c52a591e8276