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:
ruby
andnode
yarn
package 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
strict
mode. 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 onstrict
mode being enabled to produce correct type information. - The import statements in the boilerplate
hello_react.tsx
require"allowSyntheticDefaultImports": true
to work with TypeScript. This setting is activated by specifying the"esModuleInterop": true
option 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')))
})