Robert Mosolgo

Watching files during Rails development

You can tell Ruby on Rails to respond to changes in certain files during development.

Rails knows to watch config/routes.rb for changes and reload them when the files change. You can use the same mechanism to watch other files and take action when they change.

I used this feature for react-rails server rendering and for GraphQL::Pro static queries.

app.reloader

Every Rails app has a @reloader, which is a local subclass of ActiveSupport::Reloader. It’s used whenever you call reload! in the Rails console.

It’s attached to a rack middleware which calls #run! (which, in turn, calls the reload blocks if it detects changes).

config.to_prepare

You can add custom preparation hooks with config.to_prepare:

initializer :my_custom_preparation do |app|
  config.to_prepare do
    puts "Reloading now ..."
  end
end

When Rails detects a change, this block will be called. It’s implemented by registering the block with app.reloader.

app.reloaders

To add new conditions for which Rails should reload, you can add to the app.reloaders array:

# Object responds to `#updated?`
class MyWatcher
  def updated?
    # ...
  end
end

# ...

initializer :my_custom_watch_condition do |app|
  # Register custom reloader:
  app.reloaders << MyWatcher.new
end

The object’s updated? method will be called by the reloader. If any reloader returns true, the middleware will run all to_prepare blocks (via the call to @reloader.run!).

FileUpdateChecker

Rails includes a goodie for watching files. ActiveSupport::FileUpdateChecker is great for:

You can create your own FileUpdateChecker and add it to app.reloaders to reload Rails when certain files change:

# Watch specific files:
app.reloaders << ActiveSupport::FileUpdateChecker.new(["my_important_file.txt", "my_other_important_file.txt"])
# Watch directory-extension pairs, eg all `.txt` and `.md` files in `app/important_files` and subdirectories:
app.reloaders << ActiveSupport::FileUpdateChecker([], { "app/important_files" => [".txt", ".md"] })

Some filesystems support an evented file watcher implementation, ActiveSupport::EventedFileUpdateChecker. app.config.file_watcher will return the proper filewatcher class for the current context.

app.reloaders << app.config.file_watcher(["my_important_file.txt", "my_other_important_file.txt"])

All Together Now

react-rails maintains a pool of V8 instances for server rendering React components. These instances are initialized with a bunch of JavaScript code, and whenever a developer changes a JavaScript file, we need to reload them with the new code. This requires two steps:

  • Adding a new watcher to app.reloaders to detect changes to JavaScript files
  • Adding a to_prepare hook to reload the JS instances

It looks basically like this:

initializer "react_rails.watch_js_files" do |app|
  # Watch for changes to javascript files:
  app.reloaders << app.config.file_watcher.new([], {
    # Watch the asset pipeline:
    Rails.root.join("app/assets/javascripts").to_s => ["jsx", "js"],
    # Watch webpacker:
    Rails.root.join("app/javascript").to_s => ["jsx", "js"]
  })

  config.to_prepare do
    React::ServerRendering.reset_pool
  end
end

The full implementation supports some customization. You can see similar (and more complicated) examples with routes reloading, i18n reloading and .rb reloading.

Happy reloading!