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:
- The techniques to deliver and integrate JavaScript with
Rails. - How to use an Import Map to deliver JavaScript in your
Railsapp. - How to integrate JavaScript bundlers like
esbuildorrollupwithRails. - What Turbo is and how
Railsintegrates with it. - How to install Stimulus and use it for client-side JavaScript functionality.
- How to make HTTP requests using JavaScript with the
request.jslibrary.
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:
- 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-railsgem to manage your CSS.
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.