More at rubyonrails.org:

Working with JavaScript in Rails

This guide covers the integration of JavaScript into your Rails application.

After reading this guide, you will know:

  • The techniques to deliver and integrate JavaScript with Rails.
  • How to use an Import Map to deliver JavaScript in your Rails app.
  • How to integrate JavaScript bundlers like esbuild or rollup with Rails.
  • The JavaScript libraries included with Rails.
  • How to make HTTP requests using JavaScript with the request.js library.

1. Introduction

Rails applications typically deliver server-rendered HTML to the browser. In this setup, JavaScript is used mainly to add a layer of user interactivity over the HTML document.

JavaScript files can be delivered individually using an import map, or bundled together and shipped as a single file. This guide will cover the pros and cons of each approach, as well as Rails' default JavaScript stack composed of Turbo and Stimulus (which are part of the Hotwire suite).

Rails can also be used in API-mode where it speaks JSON or XML, but in such a setup, the front-end JavaScript application is usually transmitted independently from the Rails app. As such, this guide covers the usage of JavaScript for server-rendered applications only.

Modern JavaScript has a standard structure for packaging called ECMAScript modules (ESM). Rails provides two mechanisms to deliver JavaScript written for ESM: an import map, or a JavaScript bundler.

Legacy JavaScript applications may use another structure such as CommonJS. In such setups, JavaScript needs to be compiled and bundled into a single file before transmission to the browser. Common bundlers for this approach include Babel and Webpack, but Rails doesn't integrate tightly with these tools. You can, however, deliver pre-built or bespoke JavaScript files using Rails.

Rails uses the Asset Pipeline to deliver all assets, including JavaScript files, to the browser.

2. Using a JavaScript Import Map

A JavaScript import map allows JavaScript files to be delivered separately without bundling and still be able to reference each other.

It is a JSON object which defines the mapping between the module specifier passed to an import statement, and the path to the actual file to be imported. Here's an example:

<script type="importmap" data-turbo-track="reload">
{
  "imports": {
    "animations": "/assets/scripts/animations.js",
    "utilities": "/assets/scripts/utilities.js"
  }
}
</script>

Scripts can now invoke import "animations" or import "utilities" and the browser will import the corresponding file and the module it contains.

In Rails, the import map is constructed using the importmap-rails gem.

This technique doesn't require an additional build step for your JavaScript. Your files are delivered as-is, and hence no JavaScript runtime such as Node.js is required.

2.1. Installing importmap-rails

The importmap-rails gem is included by default in all new Rails applications. In older applications, you can install it using:

$ bundle add importmap-rails
$ bin/rails importmap:install

2.2. Declaring JavaScript Files

All your JavaScript files need to be declared in config/importmap.rb so Rails knows to include them in the import map object.

# config/importmap.rb

# Declare JavaScript files from your application
pin "application" # app/javascript/application.js
pin "utilities"   # app/javascript/utilities.js

# Declare all files inside a folder
pin_all_from "app/javascript/controllers", under: "controllers"

All files declared in your config/importmap.rb must exist within your asset pipeline's load paths.

This will create an import map object similar to:

<script type="importmap" data-turbo-track="reload">
{
  "imports": {
    "application": "/assets/application-d8a8613a.js",
    "utilities": "/assets/utilities-e8dc057d.js",
    "controllers/application": "/assets/controllers/application-3affb389.js",
    "controllers/hello_controller": "/assets/controllers/hello_controller-708796bd.js",
    "controllers": "/assets/controllers/index-ee64e1f1.js"
  }
}
</script>

which is rendered in your HTML document's <head> using:

<%= javascript_importmap_tags %>

It's worth reiterating that the import map only defines the mapping between a module specifier and a file. It doesn't execute or import any code. As part of the above declaration, Rails also renders:

<script type="module">import "application"</script>

This is the default entry-point for your JavaScript application (located at app/javascript/application.js). Use this file to import additional JavaScript files such as your Stimulus controllers or the utilities file shown in the above examples.

You'll notice that the filenames contain a hash. This is added by Rails' Asset Pipeline. It is calculated based on the file's contents and used to version the files.

See the Asset Pipeline guide and the importmap-rails Readme for further information.

3. Using a JavaScript Bundler

You can integrate a JavaScript bundler into Rails using the jsbundling-rails gem. It supports a number of bundlers such as ESBuild, Rollup, Bun, and Webpack.

jsbundling-rails requires a JavaScript runtime. For all bundlers except Bun, you'll need Node.js. For Bun, you'll just need to install that as it is both a JavaScript runtime and a bundler. jsbundling-rails will automatically detect the JavaScript bundler, so you may use alternatives such as Yarn without additional configuration.

3.1. Installing Node.js and Yarn

Find the installation instructions on the Node.js website and verify it’s installed correctly:

$ node --version
v23.6.1

To install Yarn, follow the installation instructions at the Yarn website. Verify it's installed using:

$ yarn --version
1.22.19

3.2. Installing Bun

Follow the installation instructions at the Bun website and verify it’s installed:

$ bun --version
v1.3.13

3.3. Installing jsbundling-rails

When creating a new Rails app, setup a JavaScript bundler using the -j or --javascript flag:

$ rails new my_new_app -j esbuild
$ rails new my_new_app --javascript=esbuild

Add the jsbundling-rails gem in an existing Rails app using:

$ bundle add jsbundling-rails

Then configure your chosen bundler with:

$ bin/rails javascript:install:[bun|esbuild|rollup|webpack]

When using jsbundling-rails, use bin/dev to start the JavaScript bundler along with Rails server in development. Further information is available in the Asset Pipeline guide.

4. Delivering Bespoke JavaScript Files

You may wish to use Rails to deliver a pre-built or bespoke JavaScript file. This is useful if your JavaScript application doesn't use ESM, or requires a bundler such as Babel which isn't natively supported within Rails.

You can reference any JavaScript files in the asset pipeline's load path using javascript_include_tag:

<%= javascript_include_tag "application" %>

which will render

<script src="/assets/application-5bcb24fe.js"></script>

A common setup in this case would be to write your built JavaScript files to app/assets/builds (which is in the asset pipeline's load path), and load them into your HTML document using javascript_include_tag.

Configuring your chosen bundler within Rails is out of scope for this guide. All supported bundlers are designed to work with ESM and are covered in the previous section: Using a JavaScript Bundler.

Files within app/assets/builds are excluded from source control by default.

5. Adding npm Packages

5.1. Vendoring npm Packages with importmap-rails

When using importmap-rails, npm packages are downloaded into the vendor folder in your app and checked into source control.

Add a package to your application using bin/importmap pin:

$ bin/importmap pin ahoy.js

This will download the package into your vendor folder and declare it in your config/importmap.rb. You can then import the package wherever required:

// app/javascript/application.js

import ahoy from "ahoy.js";

Or in a Stimulus controller:

import { Controller } from "@hotwired/stimulus"
import ahoy from "ahoy.js";

// Connects to data-controller="ahoy"
export default class extends Controller {
  connect() {
    ahoy.trackView()
  }
}

Further information is available in the importmap-rails Readme.

5.2. Installing npm Packages with a JavaScript Bundler

When using Bun, the Bun package manager installs npm packages:

$ bun add ahoy.js

See the Bun documentation for more information.

For all other bundlers, use Yarn to manage your dependencies:

$ yarn add ahoy.js

Further details are available in the Yarn documentation.

6. Choosing Between an Import Map and a JavaScript Bundler

In all new Rails apps, JavaScript is delivered using an import map. The Rails team believes that using an import map reduces complexity, improves developer experience, and delivers performance gains.

For many applications, especially those that rely primarily on Hotwire, an import map will be the right option for the long term. You can read more about the reasoning behind making import maps the default in Rails 7 here.

However, there may be use cases that call for a JavaScript bundler. Listed below are a few considerations where a JavaScript bundler may be more suited to your app than an import map:

  • You cannot serve your assets over HTTP/2.
  • Your code requires a transpilation step, such as JSX or TypeScript.
  • You need to use JavaScript libraries that include CSS or otherwise rely on Webpack loaders.
  • Your JavaScript architecture requires tree-shaking.
  • You're using the cssbundling-rails gem to manage your CSS.

7. Rails' JavaScript Libraries

Rails's default front-end stack is Hotwire which is installed by default in all new Rails apps. A utility library called request.js can be optionally installed to simplify HTTP requests using JavaScript.

7.1. Hotwire

Hotwire consists of the libraries Turbo and Stimulus which integrate with Rails via the gems turbo-rails and stimulus-rails. Refer to the Using Hotwire in Rails guide for details.

7.2. request.js

Rails protects against CSRF attacks by validating non-GET requests with a token. The request.js library automatically adds the CSRF token to HTTP requests, making it easier to trigger HTTP requests using JavaScript.

This library is maintained by the Rails team but it isn't included in Rails by default, so you'll need to install it:

$ bundle add requestjs-rails
$ bin/rails requestjs:install

Here's an example of a Stimulus controller that uses request.js to make a POST request:

import { post } from "@rails/request.js"
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = [ "input" ]

  async fetchSuggestions() {
    const response = await post("/users/suggestions", {
      body: JSON.stringify({
        input: this.inputTarget.value
      })
    })

    if (response.ok) {
      // Do something with the response
    }
  }
}

request.js will automatically activate JavaScript responses which have a content-type response header of application/javascript or application/ecmascript. It will also automatically execute Turbo Stream responses.

See the Readme for advanced usage and further installation information.

Prior to Rails 7, a JavaScript library called Rails UJS was used to enhance Rails on the front-end. This library has now been removed from Rails, and all its functionality has been replaced by Turbo, Stimulus, and request.js. You can find information about Rails UJS in an older version of the guides.



Back to top