Rails 7 with Hotwire Turbo is awesome. However, when it comes to fine-tune layouts and views, especially with Tailwind CSS, live-reload becomes a must-have feature.

I used to import the hotwire-livereload gem to achieve that. It’s a recently published gem for Hotwire Rails applications.

But it may break the app when:

  • No available Redis server. The gem has declared its Redis dependency but never check it at the initialization. Instead, it just breaks the server thread with no exception handler.

  • Live-reload disabled. Unlike the rack-mini-profiler gem, this gem requires to insert the tag and handle with disabling like this:

      <% if Rails.env.development? && !Rails.root.join('tmp/livereload-disabled.txt').exist? %>
        <%= hotwire_livereload_tags %>
      <% end %>
    
  • Previewing view components. It just breaks.

Browser-sync to save life

By contrast, Browsersync is a much more mature solution for web page live reload and even for cross-device browser testing!

It only takes seconds to setup for Rails apps with Hotwire Turbo.

0. Install and initialize

Install browser-sync as a global dependency and to use a bs-config.js for configurations.

npm install --global browser-sync
browser-sync init

1. Setup X-Forwarded-Host header for authenticity check

Rails checks the authenticity token when submitting forms for security reason. Thus, we must setup the X-Forwarded-Host header to make the proxy totally work:

proxy: {
  target: 'localhost:3000',

  proxyReq: [
    // Setup X-Forwarded-Host header for authenticity token check.
    (proxyReq) => proxyReq.setHeader('X-Forwarded-Host', 'localhost:3001')
  ]
}

2. Setup snippetOptions for Turbo Drive

Turbo Drive replace the content of <body> upon each response. Thus, we must place the browser-sync snippet at the of <head> part:

snippetOptions: {
  rule: {
    match: /<\/head>/i,
    fn: function (snippet, match) {
      return snippet + match
    }
  }
}

3. Setup files to be watched

files: [
  'app/assets/images/**/*',
  'app/assets/stylesheets/**/*',
  'app/components/**/*',
  'app/helpers/**/*',
  'app/javascript/**/*',
  'app/views/**/*',
  'config/locales/**/*'
]

Wrap it up

Here is the full content of the bs-config.js file. Run by: browser-sync start -c bs-config.js.

module.exports = {
  files: [
    'app/assets/images/**/*',
    'app/assets/stylesheets/**/*',
    'app/components/**/*',
    'app/helpers/**/*',
    'app/javascript/**/*',
    'app/views/**/*',
    'config/locales/**/*'
  ],

  proxy: {
    target: 'localhost:3000',

    proxyReq: [
      // Setup X-Forwarded-Host header for authenticity token check.
      (proxyReq) => proxyReq.setHeader('X-Forwarded-Host', 'localhost:3001')
    ]
  },
  port: 3001,

  // Use a custom rule to make compatible with Hotwire Turbo.
  snippetOptions: {
    rule: {
      match: /<\/head>/i,
      fn: function (snippet, match) {
        return snippet + match
      }
    }
  },

  // Stop the browser from automatically opening.
  open: false,

  // Don't show any notifications in the browser.
  notify: false
}