Live reloading with Ruby on Rails and esbuild06 Nov 2021
Using esbuild with Rails, via jsbundling-rails is very simple, especially in a new Rails 7 application; however, the default esbuild configuration is missing a few quality of life features. Most important among these missing features is live reloading. Out of the box, each time you change a file, you need to refresh the page to see your changes.
Once you’ve gotten used to live reloading (or its fancier cousin, Hot Module Replacement), losing it is tough.
Today, esbuild doesn’t support HMR, but with some effort it is possible to configure esbuild to support live reloading via automatic page refreshing, and that’s what we’re going to do today.
Before we get started, please note that this very much an experiment that hasn’t been battle-tested. I’m hoping that this is a nice jumping off point for discussion and improvements. YMMV.
With that disclaimer out of the way, let’s get started!
We’ll start by creating a new Rails 7 application.
If you aren’t already using Rails 7 for new Rails applications locally, this article can help you get your local environment ready.
rails new command is ready for Rails 7, from your terminal:
Here we created a new Rails application set to use
jsbundling-rails with esbuild and then generated a controller we’ll use to verify that the esbuild configuration works.
In addition to installing esbuild for us,
jsbundling-rails creates a few files that simplify starting the server and building assets for development. It also changes how you’ll boot up your Rails app locally.
Rather than using
rails s, you’ll use
bin/dev uses foreman to run multiple start up scripts, via
Procfile.dev. We’ll make a change to the
Procfile.dev later, but for now just know that when you’re ready to boot up your app, use
bin/dev to make sure your assets are built properly.
Configure esbuild for live reloading
To enable live reloading, we’ll start by creating an esbuild config file. From your terminal:
We’ll add reloading for views and CSS next, but we’ll start simpler.
esbuild-dev.config.js like this:
There’s a lot going on here, let’s walk through it a section at a time:
First we require packages and define a few variables, easy so far, right?
watchOptions will be passed to esbuild to define what happens each time an esbuild rebuild is triggered.
When there’s an error, we output the error, otherwise, we output a success message and then use
res.write to send data out to each client.
clients.length = 0 empties the
clients array to prepare it for the next rebuild.
This section defines the esbuild
The important options are the watch option, which takes the
watchOptions variables we defined earlier and
location.reload() each time a message is received from
EventSource banner and sending a new request from
8082 each time
This section at the end of the file simply starts up a local web server using node’s
With the esbuild file updated, we need to update
package.json to use the new config file:
Here we updated the
scripts section of
package.json to add a new
start script that uses our new config file. We’ve left
build as-is since
build will be used on production deployments where our live reloading isn’t needed.
Procfile.dev to use the
app/views/home/index.html.erb to connect the default
hello Stimulus controller:
Now boot up the app with
bin/dev and head to http://localhost:3000/home/index.
Then open up
connect method, maybe something like this:
If all has gone well, you should see the new Hello Peter header on the page, replacing the Hello World header.
HTML and CSS live reloading
Our basic approach will be to scrap esbuild’s watch mechanism and replace it with our own file system monitoring that triggers rebuilds and pushes updates over the local server when needed.
Install chokidar from your terminal with:
With chokidar installed, we’ll update
esbuild-dev.config.js again, like this:
Again, lots going on here. Let’s step through the important bits.
First, we require
chokidar, which we need to setup file system watching. Starting easy again.
Next, we setup the
Here we’ve moved the
build setup into an async function that assigns
We also added the
incremental flag to the builder, which makes repeated builds (which we’ll be doing) more efficient.
watch option was removed since we no longer want esbuild to watch for changes on rebuild on its own.
Next, we setup
Finally, we send a request out from our local server, notifying the browser that it should reload the current page.
With these changes in place, stop the server if it is running and then
bin/dev again. Open up or refresh http://localhost:3000/home/index, make changes to
application.css and see that those changes trigger page reloads and that updating
hello_controller.js still triggers a reload.
Today we created an esbuild config file that enables live reloading (but not HMR) for our jsbundling-rails powered Rails application. As I mentioned at the beginning of this article, this is very much an experiment and this configuration has not been tested on an application of any meaningful size. You can find the finished code for this example application on Github.
I’m certain that there are better routes out there to the same end result, and I’d love to hear from others on pitfalls to watch out for and ways to improve my approach.
While researching this problem, I leaned heavily on previous examples of esbuild configs. In particular, the examples found at these two links were very helpful in getting live reload to a functional state:
- This example esbuild config, from an issue on the jsbundling-rails Github repo
- This discussion on the esbuild Github repo
If you, like me, are a Rails developer that needs to learn more about bundling and bundlers, a great starting point is this deep dive into the world of bundlers. If you’re intested in full HMR without any speed loss, and you’re willing to break out of the standard Rails offerings, you might enjoy vite-ruby.
Finally, if you’re using esbuild with Rails and Stimulus, you’ll probably find the esbuild-rails plugin from Chris Oliver useful.
That’s all for today. As always - thanks for reading!