123456789_123456789_123456789_123456789_123456789_

DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.

Working with JavaScript in Rails

This guide covers the integration of JavaScript into your Rails application — including the usage of Turbo and Stimulus as well as the installation of external JavaScript packages into Rails.

After reading this guide, you will know:


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 ofTurbo andStimulus](#stimulus) (which are part of the [Hotwire suite).

NOTE: Rails can also beused 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 calledECMAScript modules (ESM). Rails provides two mechanisms to deliver JavaScript written for ESM: animport map, or aJavaScript bundler.

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

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

Using a JavaScript Import Map

A JavaScriptimport 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 theimportmap-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.

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

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"

NOTE: All files declared in your config/importmap.rb must exist within yourasset 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 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.

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

See theAsset Pipeline guide and theimportmap-rails Readme for further information.

Using a JavaScript Bundler

You can integrate a JavaScript bundler into Rails using thejsbundling-rails</a> gem. It supports a number of bundlers such asESBuild,Rollup](https://rollupjs.org/guide/en/), [Bun, andWebpack.

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.

Installing Node.js and Yarn

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

$ node --version
v23.6.1

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

$ yarn --version
1.22.19

Installing Bun

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

$ bun --version
v1.3.13

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 theAsset Pipeline guide.

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 asBabel which isn't natively supported within Rails.

You can reference any JavaScript files in theasset 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.

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

Adding npm Packages

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 them 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 theimportmap-rails Readme.

Installing npm Packages with a JavaScript Bundler

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

$ bun add ahoy.js

See theBun documentation for more information.

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

$ yarn add ahoy.js

Further details are available in theYarn documentation.

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 onHotwire, 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 7here.

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:

Hotwire

Rails' default JavaScript stack isHotwire. It is a suite of front-end libraries that enable us to build rich, high-fidelity, and modern web applications without the complexities of a single-page application.

Turbo and Stimulus which are part of the Hotwire suite are automatically installed in all new Rails apps.

This guide primarily covers Rails' integration with Turbo and Stimulus. Consult their documentation for detailed usage information:

Turbo

Turbo is the nucleus of Hotwire. It consists of 3 parts: Turbo Drive, Turbo Frames, and Turbo Streams.

Turbo Drive accelerates links and form submissions by making those requests using JavaScript and swapping out the document's element, eliminating the need for full page loads.

Turbo Frames allow you to decompose pages into independent contexts where navigation and updates can occur without affecting the rest of the page.

Turbo Streams are used to make fine-grained, targeted updates to specific DOM elements using a range of CRUD actions.

Rails integrates with Turbo via the [turbo-rails][] gem. You can use this gem to install Turbo in existing applications:

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

See theTurbo handbook for more information on how Turbo works and its features.

Turbo Drive

Turbo Drive largely works automatically when imported into your HTML document. It offers a few configuration options, and the ability to define data- attributes and tags in your HTML to customize behavior. See thehandbook andreference for further details.

Rails offers view helper methods via theturbo-rails gem which define tags to customize Turbo Drive on specific pages.

You cancontrol a page's caching behavior by setting a turbo-cache-control meta tag.

<%# Renders <meta name="turbo-cache-control" content="no-cache"> %>
<%= turbo_exempts_page_from_cache %>

<%# Renders <meta name="turbo-cache-control" content="no-preview"> %>
<%= turbo_exempts_page_from_preview %>

Force afull page reload for specific pages with:

<%# Renders <meta name="turbo-visit-control" content="reload"> %>
<%= turbo_page_requires_reload %>

Configuremorphing page refreshes with:

<%= turbo_refreshes_with(method: :morph, scroll: :preserve) %>

View thesource code for more details.

Turbo Frames

Turbo Frames uses a element to isolate parts of a web page into its own navigation context, so it can be updated independently from the rest of the page.

The [turbo-rails][] gem defines a helper method to simplify the declaration of a <turbo-frame>:

<%= turbo_frame_tag dom_id(post) do %>
  <div>
     <%= link_to post.title, post_path(post) %>
  </div>
<% end %>

All Turbo Frame elements require a unique ID. Thedom_id</a> method calculates an ID based on an Active Record object and is commonly used to identify Turbo Frames.

Turbo Streams

Turbo Streams are used to perform a series of actions (such as create, append, remove, replace etc.) on specific DOM elements via a element. As soon as a tag is added to the document, Turbo will execute it and perform the action it defines.

[turbo-rails][] provides helpers to create HTTP responses consisting of Turbo Streams, as well as an integration withAction Cable to allow Turbo Streams to be delivered via WebSockets.

Render a Turbo Stream in your controller using:

def create
  @post = Post.new(post_params)

  respond_to do |format|
    if @post.save
      format.turbo_stream do
        # Renders:
        # <turbo-stream action="prepend" target="posts">
        #   <template>
        #     <h2>My New Post</h2>
        #   </template>
        # </turbo-stream>
        render turbo_stream: turbo_stream.prepend("posts", helpers.tag.h2(@post.title))
      end
    else
      format.html { render :new, status: :unprocessable_entity }
    end
  end
end

You can use an ERB template as well. This is useful when defining multiple Turbo Streams:

def create
  @post = Post.new(post_params)

  respond_to do |format|
    if @post.save
      format.turbo_stream
    else
      format.html { render :new, status: :unprocessable_entity }
    end
  end
end
<%# create.turbo_stream.erb %>

<%= turbo_stream.prepend("posts", partial: "posts/post", locals: { post: @post }) %>
<%= turbo_stream.replace("posts_title") do %>
  <%= Post.count %> posts
<% end %>

Turbo Streams over Action Cable

To deliver Turbo Streams over WebSockets, ensure thatAction Cable is set up in your application and you have the [turbo-rails][] JavaScript package installed.

Turbo Streams can be received over WebSockets by subscribing to broadcasts on a stream within a view:

<%= turbo_stream_from "posts" %>

This will render a tag which opens a WebSocket connection and subscribes to a stream called "posts". It will automatically execute any Turbo Streams it receives.

Broadcast a Turbo Stream action to this stream using:

Turbo::StreamsChannel.broadcast_action_to(
  "posts",
  action: :append,
  target: "posts",
  partial: "posts/post",
  locals: { post: post }
)

There are helper methods to broadcast the stock Turbo Stream actions. The above snippet can be rewritten as:

Turbo::StreamsChannel.broadcast_append_to(
  "posts",
  target: "posts",
  partial: "posts/post",
  locals: { post: post }
)

You can also broadcast a Turbo Stream template containing multiple actions:

Turbo::StreamsChannel.broadcast_render_to(
  "posts",
  template: "posts/create"
)

or broadcast a refresh action which is useful for morphing:

Turbo::StreamsChannel.broadcast_refresh_to("posts")

All the above examples render templates and broadcast them synchronously. They can be offloaded to a background job to improve performance by using the later version of the methods such as broadcast_append_later_to.

# enqueues a `Turbo::Streams::ActionBroadcastJob`
Turbo::StreamsChannel.broadcast_append_later_to(
  "posts",
  target: "posts",
  partial: "posts/post",
  locals: { post: post }
)

# enqueues a `Turbo::Streams::BroadcastJob`
Turbo::StreamsChannel.broadcast_render_later_to(
  "posts",
  template: "posts/create"
)

Check out thesource code for all available helpers.

In addition to this, the gem provides aBroadcastable</a> concern which is included in Active Record. It applies Rails conventions to succinctly broadcast model-specific Turbo Streams. Some example use cases are:

@post = Post.first

# Turbo Stream actions are implicitly broadcast to
# the model object's stream. To subscribe to an individual
# model's stream, you'd use:
#
# <%= turbo_stream_from @post %>

# Broadcasts an `append` action containing the partial
# `posts/post` targeted at the DOM ID `posts`.
@post.broadcast_append
@post.broadcast_append_later

# The update action targets the specific model's HTML element (`dom_id(@post)`).
# In this case, it will target the DOM ID `post_1`. The content
# will be the partial `posts/post`.
@post.broadcast_update
@post.broadcast_update_later

# The remove action targets the specific model's HTML element (`dom_id(@post)`).
# In this case, it will target the DOM ID `post_1`.
@post.broadcast_remove
@post.broadcast_remove_later

# The partial can be explicitly defined if required.
@post.broadcast_append(partial: "posts/post", locals: { post: @post })
@post.broadcast_append_later(partial: "posts/post", locals: { post: @post })

# Broadcast to a specific stream
@post.broadcast_append_to("posts")
@post.broadcast_append_later_to("posts")

The broadcasts_to method configures a model to emit Turbo Streams on creation, update, and deletion to the supplied stream name:

class Post < ApplicationRecord
  broadcasts_to ->(post) { post.model_name.plural }
end

The above snippet is equivalent to:

class Post < ApplicationRecord
  after_create_commit  -> { broadcast_append_later_to("posts", target: "posts", partial: "posts/post") }
  after_update_commit  -> { broadcast_replace_later_to("posts", target: dom_id(self), partial: "posts/post") }
  after_destroy_commit -> { broadcast_remove_to("posts", target: dom_id(self)) }
end

Use broadcasts to emit Turbo Streams to an inferred stream name:

class Post < ApplicationRecord
  broadcasts
end

This can be expanded as:

class Post < ApplicationRecord
  after_create_commit  -> { broadcast_append_later_to("posts", target: "posts", partial: "posts/post") }
  after_update_commit  -> { broadcast_replace_later_to(self, target: dom_id(self), partial: "posts/post") }
  after_destroy_commit -> { broadcast_remove_to(self, target: dom_id(self)) }
end

Use broadcasts_refreshes to emit a Turbo Stream to refresh the page whenever the model changes:

class Post < ApplicationRecord
  broadcasts_refreshes
end

The above code is equivalent to:

class Post < ApplicationRecord
  after_create_commit  -> { broadcast_refresh_later_to("posts") }
  after_update_commit  -> { broadcast_refresh_later_to(dom_id(self)) }
  after_destroy_commit -> { broadcast_refresh_to(dom_id(self)) }
end

See thesource code and inline RDoc comments for all available helpers and options.

Stimulus

Stimulus is a lightweight library to manipulate HTML with reusable pieces of JavaScript logic encapsulated in a JavaScript controller.

Stimulus has an HTML-centric way of writing JavaScript. The markup is connected to the controller using a range of data- attributes.

Here's an example of a Stimulus controller:

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

export default class extends Controller {
  static targets = [ "name", "output" ]

  greet() {
    this.outputTarget.textContent =
      `Hello, ${this.nameTarget.value}!`
  }
}

The above controller uses targets, which are named references to elements in its HTML scope, to grab an input's value and display a greeting. The greet() action reads the name target and writes it into the output target.

It can be attached to the DOM via the data-controller attribute:

<div data-controller="hello">
  <input data-hello-target="name" type="text">

  <button data-action="click->hello#greet">Greet</button>

  <span data-hello-target="output"> </span>
</div>

Refer to the Stimulushandbook andreference for complete usage information.

Rails integrates with Stimulus via the [stimulus-rails][] gem, which provides a generator to create Stimulus controllers:

# Generates app/javascript/controllers/hello_controller.js
$ bin/rails generate stimulus hello
// app/javascript/controllers/hello_controller.js

import { Controller } from "@hotwired/stimulus"

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

It also contains a task which you can use to install Stimulus in an existing application:

$ bin/rails stimulus:install

request.js

Rails protects againstCSRF attacks byvalidating non-GET requests with a token. Therequest.js</a> 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 theReadme for advanced usage and further installation information.

NOTE: 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 anolder version of the guides.