Working with JavaScript in Rails
This guide covers the options for integrating JavaScript functionality into your Rails application,including the options you have for using external JavaScript packages and how to use Turbo withRails.
After reading this guide, you will know:
- How to use Rails without the need for a Node.js, Yarn, or a JavaScript bundler.
- How to create a new Rails application using import maps, Bun, esbuild, Rollup, or Webpack to bundleyour JavaScript.
- What Turbo is, and how to use it.
- How to use the Turbo HTML helpers provided by Rails.
1. Import Maps
Import maps let you import JavaScript modules usinglogical names that map to versioned files directly from the browser. Import maps are the defaultfrom Rails 7, allowing anyone to build modern JavaScript applications using most npm packageswithout the need for transpiling or bundling.
Applications using import maps do not needNode.js orYarn to function. If you plan to use Rails withimportmap-rails tomanage your JavaScript dependencies, there is no need to install Node.js or Yarn.
When using import maps, no separate build process is required, just start your server withbin/rails server and you are good to go.
1.1. Installing importmap-rails
Importmap for Rails is automatically included in Rails 7+ for new applications, but you can also install it manually in existing applications:
$bundleadd importmap-railsRun the install task:
$bin/railsimportmap:install1.2. Adding npm Packages with importmap-rails
To add new packages to your import map-powered application, run thebin/importmap pin commandfrom your terminal:
$bin/importmap pin react react-domThen, import the package intoapplication.js as usual:
importReactfrom"react"importReactDOMfrom"react-dom"2. Adding npm Packages with JavaScript Bundlers
Import maps are the default for new Rails applications, but if you prefer traditional JavaScriptbundling, you can create new Rails applications with your choice ofBun,esbuild,Webpack, orRollup.js.
To use a bundler instead of import maps in a new Rails application, pass the--javascript or-joption torails new:
$railsnew my_new_app--javascript=bunOR$railsnew my_new_app-j bunThese bundling options each come with a simple configuration and integration with the assetpipeline via thejsbundling-rails gem.
When using a bundling option, usebin/dev to start the Rails server and build JavaScript fordevelopment.
2.1. Installing a JavaScript Runtime
If you are using esbuild, Rollup.js, or Webpack to bundle your JavaScript inyour Rails application, Node.js and Yarn must be installed. If you are usingBun, then you just need to install Bun as it is both a JavaScript runtime and a bundler.
2.1.1. Installing Bun
Find the installation instructions at theBun website andverify it’s installed correctly and in your path with the following command:
$bun--versionThe version of your Bun runtime should be printed out. If it says somethinglike1.0.0, Bun has been installed correctly.
If not, you may need to reinstall Bun in the current directory or restart your terminal.
2.1.2. Installing Node.js and Yarn
If you are using esbuild, Rollup.js, or Webpack you will need Node.js and Yarn.
Find the installation instructions at theNode.js website andverify it’s installed correctly with the following command:
$node--versionThe version of your Node.js runtime should be printed out. Make sure it’s greater than8.16.0.
To install Yarn, follow the installation instructions at theYarn website. Running this command should print outthe Yarn version:
$yarn--versionIf it says something like1.22.0, Yarn has been installed correctly.
3. Choosing Between Import Maps and a JavaScript Bundler
When you create a new Rails application, you will need to choose between import maps and aJavaScript bundling solution. Every application has different requirements, and you shouldconsider your requirements carefully before choosing a JavaScript option, as migrating from oneoption to another may be time-consuming for large, complex applications.
Import maps are the default option because the Rails team believes in import maps' potential forreducing complexity, improving developer experience, and delivering performance gains.
For many applications, especially those that rely primarily on theHotwirestack for their JavaScript needs, import maps will be the right option for the long term. Youcan read more about the reasoning behind making import maps the default in Rails 7here.
Other applications may still need a traditional JavaScript bundler. Requirements that indicatethat you should choose a traditional bundler include:
- If your code requires a transpilation step, such as JSX or TypeScript.
- If you need to use JavaScript libraries that include CSS or otherwise rely onWebpack loaders.
- If you are absolutely sure that you needtree-shaking.
- If you will install Bootstrap, Bulma, PostCSS, or Dart CSS through thecssbundling-rails gem. All options provided by this gem except Tailwind and Sass will automatically install
esbuildfor you if you do not specify a different option inrails new.
4. Turbo
Whether you choose import maps or a traditional bundler, Rails ships withTurbo to speed up your application while dramatically reducing theamount of JavaScript that you will need to write.
Turbo lets your server deliver HTML directly as an alternative to the prevailing front-endframeworks that reduce the server-side of your Rails application to little more than a JSON API.
4.1. Turbo Drive
Turbo Drive speeds up page loads by avoiding full-pageteardowns and rebuilds on every navigation request. Turbo Drive is an improvement on andreplacement for Turbolinks.
4.2. Turbo Frames
Turbo Frames allow predefined parts of a page to beupdated on request, without impacting the rest of the page’s content.
You can use Turbo Frames to build in-place editing without any custom JavaScript, lazy loadcontent, and create server-rendered, tabbed interfaces with ease.
Rails provides HTML helpers to simplify the use of Turbo Frames through theturbo-rails gem.
Using this gem, you can add a Turbo Frame to your application with theturbo_frame_tag helperlike this:
<%=turbo_frame_tagdom_id(post)do%><div><%=link_topost.title,post_path(post)%></div><%end%>4.3. Turbo Streams
Turbo Streams deliver page changes as fragments ofHTML wrapped in self-executing<turbo-stream> elements. Turbo Streams allow you to broadcastchanges made by other users over WebSockets and update pieces of a page after a form submissionwithout requiring a full page load.
Rails provides HTML and server-side helpers to simplify the use of Turbo Streams through theturbo-rails gem.
Using this gem, you can render Turbo Streams from a controller action:
defcreate@post=Post.new(post_params)respond_todo|format|if@post.saveformat.turbo_streamelseformat.html{render:new,status: :unprocessable_entity}endendendRails will automatically look for a.turbo_stream.erb view file and render that view when found.
Turbo Stream responses can also be rendered inline in the controller action:
defcreate@post=Post.new(post_params)respond_todo|format|if@post.saveformat.turbo_stream{renderturbo_stream:turbo_stream.prepend("posts",partial:"post")}elseformat.html{render:new,status: :unprocessable_entity}endendendFinally, Turbo Streams can be initiated from a model or a background job using built-in helpers.These broadcasts can be used to update content via a WebSocket connection to all users, keepingpage content fresh and bringing your application to life.
To broadcast a Turbo Stream from a model, combine a model callback like this:
classPost<ApplicationRecordafter_create_commit{broadcast_append_to("posts")}endWith a WebSocket connection set up on the page that should receive the updates like this:
<%=turbo_stream_from"posts"%>5. Replacements for Rails/UJS Functionality
Rails 6 shipped with a tool called UJS (Unobtrusive JavaScript). UJS allowsdevelopers to override the HTTP request method of<a> tags, to add confirmationdialogs before executing an action, and more. UJS was the default before Rails7, but it is now recommended to use Turbo instead.
5.1. Method
Clicking links always results in an HTTP GET request. If your application isRESTful, some links are in factactions that change data on the server, and should be performed with non-GETrequests. Thedata-turbo-method attribute allows marking up such links withan explicit method such as "post", "put", or "delete".
Turbo will scan<a> tags in your application for theturbo-method data attribute and use thespecified method when present, overriding the default GET action.
For example:
<%=link_to"Delete post",post_path(post),data:{turbo_method:"delete"}%>This generates:
<adata-turbo-method="delete"href="...">Delete post</a>An alternative to changing the method of a link withdata-turbo-method is to use Railsbutton_to helper. For accessibility reasons, actual buttons and forms are preferable for anynon-GET action.
5.2. Confirmations
You can ask for an extra confirmation from the user by adding adata-turbo-confirmattribute on links and forms. On link click or form submit, the user will bepresented with a JavaScriptconfirm() dialog containing the attribute's text.If the user chooses to cancel, the action doesn't take place.
For example, with thelink_to helper:
<%=link_to"Delete post",post_path(post),data:{turbo_method:"delete",turbo_confirm:"Are you sure?"}%>Which generates:
<ahref="..."data-turbo-confirm="Are you sure?"data-turbo-method="delete">Delete post</a>When the user clicks on the "Delete post" link, they will be presented with an"Are you sure?" confirmation dialog.
The attribute can also be used with thebutton_to helper, however it must beadded to the form that thebutton_to helper renders internally:
<%=button_to"Delete post",post,method: :delete,form:{data:{turbo_confirm:"Are you sure?"}}%>5.3. Ajax Requests
When making non-GET requests from JavaScript, theX-CSRF-Token header is required.Without this header, requests won't be accepted by Rails.
This token is required by Rails to prevent Cross-Site Request Forgery (CSRF) attacks. Read more in thesecurity guide.
Rails Request.JS encapsulates the logicof adding the request headers that are required by Rails. Justimport theFetchRequest class from the package and instantiate itpassing the request method, url, options, then callawait request.perform()and do what you need with the response.
For example:
import{FetchRequest}from'@rails/request.js'....asyncmyMethod(){constrequest=newFetchRequest('post','localhost:3000/posts',{body:JSON.stringify({name:'Request.JS'})})constresponse=awaitrequest.perform()if(response.ok){constbody=awaitresponse.text}}When using another library to make Ajax calls, it is necessary to add thesecurity token as a default header yourself. To get the token, have a look at<meta name='csrf-token' content='THE-TOKEN'> tag printed bycsrf_meta_tags in your application view. You could do something like:
document.head.querySelector("meta[name=csrf-token]")?.content