Configuring Ruby on Rails to Support React + TypeScript with Webpacker
This guide covers how to configure a new Ruby on Rails (RoR) v6 application to support a React front-end written in TypeScript using Webpacker.
Update: Rails 7 has been released and Webpacker has been retired as the official solution for bundling JavaScript and TypeScript. Check out the latest Rails Guide.
Code from this post is available on GitHub.
We’ll follow a common approach to combining Rails and React to ensure that deployments are supported by leading “white glove” hosting platforms like Heroku that are widely used in the Rails community.
The adoption of React + TypeScript can help modernize legacy monolithic RoR apps and open a wider talent pool of developers.
A key advantage of React is that it is relatively straightforward to introduce it into legacy apps in phases, similar to how Facebook (Meta) first rolled it out when they created the library as an internal solution to build complex UI’s.
Step-by-Step Guide
Prerequisites
Ensure that your workstation is ready with:
rubyandnodeyarnpackage manager (RoR’s preferred package manager for JavaScript)
Install rails in your ruby environment if you haven’t already done so:
gem install rails -v 6.1
In this guide we’ll use 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 command adds and configures dependencies for transpiling TypeScript to JavaScript using Babel via the @babel/preset-typescript package.
Docs: Webpacker docs for TypeScript.
Prettier + editorconfig
It is a good idea to add a configuration for prettier along with an .editorconfig file to help improve code consistency and code quality.
VSCode users can add support for the EditorConfig format by installing the EditorConfig Extension.
Here’s an example .editorconfig that can be added to the project root:
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
Here’s an example .prettierrc config file for Prettier with opinionated settings:
{
"parser": "typescript",
"singleQuote": true,
"trailingComma": "all",
"semi": false,
"bracketSpacing": true,
"tabWidth": 2,
"useTabs": false,
"printWidth": 120
}
Prettier is a powerful tool with multiple config formats, a plugin ecosystem, and more. Check the prettier docs to learn more about how to customize it further.
Type Checking
Type checking is important to actually reap any benefits from using TypeScript. We want the TypeScript compiler to fail the build if it detects any type errors.
At the time of writing type checking must be manually added to Webpack’s compilation process. Start by adding the following plugin:
yarn add fork-ts-checker-webpack-plugin --dev
The plugin can then be added to the development environment specified in 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()
TypeScript eliminates the need for the prop-types package that’s often used with React written in JavaScript. To remove the package:
yarn remove prop-types
Rename the boilerplate app/javascript/packs/hello_react.jsx to app/javascript/packs/hello_react.tsx.
Refactor the hello_react.tsx file from JS to TS 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 code editor might indicate that the some of the syntax in the above code block isn’t valid.
TypeScript’s behaviour is controlled by its config file tsconfig.json.
The following example tsconfig.json incorporates several improvements vs. the boilerplate. You can cherry-pick individual property changes that you agree with or replace your entire tsconfig.json with the 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
}
Some notable tweaks vs. the boilerplate include:
- Enabling
strictmode. TypeScript isn’t TypeScript without strict mode and without it the compiler will allow preventable bugs that it is capable of detecting with more rigorous analysis. Many popular libraries actually depend onstrictmode being enabled to produce correct type information. - The import statements in the boilerplate
hello_react.tsxrequire"allowSyntheticDefaultImports": trueto work with TypeScript. This setting is activated by specifying the"esModuleInterop": trueoption which is more broad. - A number of options (beginning with
strict*andno*) are added to improve code standards + quality, and to help eliminate certain classes of bugs.
Add a Controller with an 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.
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 (not the file itself) 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 coding in React
For a conventional foundation of a React SPA (Single Page App), create the directory app/javascript/components and add 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')))
})