Rails 7: Up & Running with JSBundling

Rails new has new options. They are --css and --javascript. If you don’t use either one, you will build a new Rails app using ImportMap. If you specify either –css or –javascript, your app will be created not using Importmap and instead using Bundling: both cssbundling-rails and jsbundling-rails.

If you are familiar with Webpacker under Rails 7, JSBundling is probably what you will transition to, because it lets you build javascript using the tool of your choice: Webpack, ESBuild, or Rollup.

Remember, if you want parity with what you were familiar with in Webpacker under Rails 6, using jsbundling-rails with the new faster esbuild is the way to go for you.

This article will help you if you’ve already decided you don’t want ImportMap. To help make that decision, check out Rails 7 : Do I need ImportMap-rails

Bundling itself is not compatible with eager loading your Javascript files, so for Stimulus, your application loader will look different.

Bundling also requires that you run a “watcher” in the background so that as you develop SCSS or Typescript your changes are immediately recompiled by the watcher and visible in your development environment.

IMPORTANT: Do not attempt to add jsbundling-rails on top of an app created with rails new (importmap). Do not attempt to create a new Rails app with cssbundling-rails and then add jsbundling-rails after. Do not attempt to switch between jsbundling-rails and Importmap on new Rails installations.

Section 1: Meet the CSS and JS Flags

If you use either the --css or the --js flag, you will get both cssbundling and jsbundling. However, if you use one or the other, the next one becomes more tedious to set up. Therefore, I recommend you always start with both --css and --javascript flags:

rails new HelloWorld --javascript=esbuild --database=postgresql

Your JS choices are: ESBuild, Rollup, and Webpack. For CSS, see Section 2 of this article.

Now make sure to use the new ./bin/dev instead of the old rails server or else your JS & CSS will not compile.

On the project folder you now start your Rails server and a background “watcher” with:


Confirm ESBuild Loads the Rails 7 Features

1. Confirm that Stimulus works

Uncomment this line in routes.rb

Rails.application.routes.draw do
  root "articles#index"

Generate an articles controller

rails generate controller Articles

Now create a file at app/views/articles/index.erb

<div data-controller="hello"></div>

Open up app/javascript/controllers/hello_controller.js

Boot rails using

./bin/dev rails


Confirm that you see the “Hello world!” on the screen.

When creating a new Stimulus controller, it is essential to register this controller with the Stimulus loader found in app/javascript/controllers/index.js

You can do this one of two ways

Generating the controller using the Rails helper:

./bin/rails generate stimulus controllerName

When generated this way, you will automatically be modifying the manifest file.

If you create the Stimulus controller by hand, run this command to update your manifest file:

./bin/rails stimulus:manifest:update

2. Confirm that Turbo is Working

Go back to routes.rb and add this line. This is non-standard, do not do this in a real Rails app!

Rails.application.routes.draw do

  post "/", to: "articles#index"
  root "articles#index"

Now go back to app/views/articles/index.erb

You can leave the Stimulus test in place, and add this form:

<div data-controller="hello"></div>

<%= form_with(url: "/") do |form| %>
  this is a form for you to submit
  <%= form.text_field :xyz %>
  <%= form.submit "submit" %>
<% end %>

Now, load your browser window and you will see the new form.

If Turbo Rails is NOT installed correctly, you will see this:

WRONG: This is what happens when you first boot without the “watcher” running — Stimulus & Turbo are not enabled

Rails 7 boots but Turbo does not initialize

Notice that this form submission — which does absolutely nothing— should be sent to the Rails backend as a TURBO_STREAM, not as an HTML request.

CORRECT: This is what happens when you boot using the new ./bin/dev rails command, which includes running webpack in the background to compile your JS.

./bin/dev rails

Rails 7 boots correctly with ./bin/dev rails

You must see the request submitted as TURBO_STREAM or else Turbo will not be working correctly.

Please note that obviously, even if Turbo is working, this app doesn’t actually do anything. The only purpose of this exercise is to examine your Rails logs for the TURBO_STREAM requests from the frontend.

Section 2: Bundling app with With Bootstrap & ESBuild

If you started with section 1, you can archive that work and start again here.

rails new MyGreatApp --javascript=esbuild --css=bootstrap --database=postgresql

1. Hello sassc-rails Gem

Start by uncommenting the scssc-rails gem which is commented out by default in the Gemfile:

gem "sassc-rails"

2. Turn on Inline Source maps (so we can debug Rails 7 Bootstrap)

in config/environments/development.rb add this line:

 config.sass.inline_source_maps = true

3. Add a (temporary) Dummy Controller & Route

Create the articles controller with:

rails generate controller Articles

Add an empty def index to it:

class ArticlesController < ApplicationController
  def index

in config/routes.rb, uncomment this line:

root "articles#index"

And finally, create a Hello World file at app/views/articles/index.erb

Hello World!

Now, load your website in your browser and click “View Source”

Sprockets has included what we told it to in our case just the @import statement

Notice here that Sprockets has applied a thumbprint to this specific build. When we change anything in our CSS, Sprockets will recompile the application file and give it a new thumbprint.

7. Now add Something for Rails 7 Bootstrap

If we look in our browser, our existing layout looks like so:

The shrewd eye might notice that Bootstrap is in fact applied (you can tell because the font is not the browser’s default), but we would hardly notice since all our app does is say “Hello World”‘

So let’s add a Bootstrap navbar:

    <nav class="navbar navbar-expand-lg navbar-light bg-light">
    <div class="container-fluid">
      <a class="navbar-brand" href="#">Navbar</a>
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
          <li class="nav-item">
            <a class="nav-link active" aria-current="page" href="#">Home</a>
          <li class="nav-item">
            <a class="nav-link" href="#">Link</a>
          <li class="nav-item dropdown">
            <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
            <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
              <li><a class="dropdown-item" href="#">Action</a></li>
              <li><a class="dropdown-item" href="#">Another action</a></li>
              <li><hr class="dropdown-divider"></li>
              <li><a class="dropdown-item" href="#">Something else here</a></li>
          <li class="nav-item">
            <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
        <form class="d-flex">
          <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
          <button class="btn btn-outline-success" type="submit">Search</button>

Ok now let’s take a look:

The second way to confirm that Javascript is working is to collapse your browser window. This special navbar display collapses into a Hamburger menu, and its slide-down effect requires that the popper-js and jQuery (javascript libraries) be correctly loaded through JS-bundling (ESbuild, in this case).

Notice that when you hit the Hamburger menu, the other options slide down.

Even though jQuery is available to Bootstrap here via importing it through the Webpack dependency management, jQuery is not globally available as was custom with old-style Rails development.

This is because we are now using the ESModules paradigm of modern Javascript, where we import the jQuery only where we need it.

Hence, if you open your console browser and type jQuery, you will see:

This is the expected result as jQuery is not globally available on your page