Autoloading
Ruby on Rails Software Development
July 11, 2022 - Robert C.

Autoloading for Faster Development in Ruby on Rails

One of the things about the bootstrap helpers gem that is a bit annoying is that every time you change a helper, you have to reload the rails server to see the changes. That’s a bit annoying, takes time from developing, and can add time to a project timeline, so let’s try to fix that.

Why You’d Consider Autoloading in Ruby on Rails

Rails does a good job of autoloading most things but it has it’s limits. As stated, we don’t want to have to reload the todo application every time we change the bootstrap helpers gem. By default, Ruby on Rails will refresh the views, but this does not include the classes or the helpers. Getting the classes to refresh isn’t that hard or cryptic. But refreshing the helpers; well that can get a bit complicated.

# lib/bootstrap_helpers/railtie.rb

module BootstrapHelpers
  class Railtie < Rails::Railtie
    initializer 'bootstrap_helpers.view_helpers' do
      ActiveSupport.on_load(:action_view) { include Bootstrap::BaseHelper }
      ActiveSupport.on_load(:action_view) { include Bootstrap::NavBarHelper }
      ActiveSupport.on_load(:action_view) { include Bootstrap::GridHelper }
      ActiveSupport.on_load(:action_view) { include Bootstrap::ErrorHelper }
      ActiveSupport.on_load(:action_view) { include Bootstrap::ProgressHelper }
      ActiveSupport.on_load(:action_view) { include Bootstrap::ErrorHelper }
      ActiveSupport.on_load(:action_view) { include Bootstrap::AlertHelper }
      ActiveSupport.on_load(:action_view) { include Bootstrap::IconHelper }
      bootstrap_helpers_load_helpers
    end
  end
end

That is the largest part of the problem. We load the helpers on initialize, so they only get updated on the initial load. But here’s the hard part: we really need them to be loaded like that. If you don’t have them parsed on the initial load, then you won’t have all the nice helpers without making modifications to the parent application.

Using Autoloading to Fix It

Part 1

In newer versions of Rails the Zeitwerk library is used to manage auto-loading and reloading. So the first part is to hook into that a bit and get the classes to reload on change.

require 'listen'
require 'zeitwerk'

loader = Zeitwerk::Loader.for_gem
loader.enable_reloading
loader.setup

Listen.to('lib', "#{__dir__}/bootstrap_helpers/helpers", "#{__dir__}/bootstrap_helpers") { loader.reload }.start

First, you can see that were using listen and Zeitwerk. listen is really good at doing things when files change. So our little one-liner fires off Zzeitwerks reloader on file change.

This gets us close except our name spaces are off. We need to change our helpers a tiny bit to match the structure that Zeitwerk expects.

# lib/bootstrap_helpers/helpers/icon_helper.rb
module BootstrapHelpers
  module Helpers
    module IconHelper

Now, with those changes in place, whenever we change a file, the objects are reloaded and everyone is happy, right? Well almost…

Part 2

Remember back in the problem?

# lib/bootstrap_helpers/railtie.rb

module BootstrapHelpers
  class Railtie < Rails::Railtie
    initializer 'bootstrap_helpers.view_helpers' do
      ActiveSupport.on_load(:action_view) { include Bootstrap::BaseHelper }
      ActiveSupport.on_load(:action_view) { include Bootstrap::NavBarHelper }
      ActiveSupport.on_load(:action_view) { include Bootstrap::GridHelper }
      ActiveSupport.on_load(:action_view) { include Bootstrap::ErrorHelper }
      ActiveSupport.on_load(:action_view) { include Bootstrap::ProgressHelper }
      ActiveSupport.on_load(:action_view) { include Bootstrap::ErrorHelper }
      ActiveSupport.on_load(:action_view) { include Bootstrap::AlertHelper }
      ActiveSupport.on_load(:action_view) { include Bootstrap::IconHelper }
      bootstrap_helpers_load_helpers
    end
  end
end

We have an issue. The classes (aside from having the wrong name now) are still only defined on init. Well, it’s great that IconHelper is now reloaded when I change it, but that doesn’t do any good, as the already loaded version is already included.

The adjustment to that is to extend our listen block a little bit and include the new version.

# lib/bootstrap_helpers.rb

Listen.to('lib', "#{__dir__}/bootstrap_helpers/helpers", "#{__dir__}/bootstrap_helpers") do
  loader.reload
  bootstrap_helpers_load_helpers
end.start

require 'bootstrap_helpers/railtie' if defined?(Rails)

def bootstrap_helpers_load_helpers
  ActiveSupport.on_load(:action_view) { include BootstrapHelpers::Helpers::BaseHelper }
  ActiveSupport.on_load(:action_view) { include BootstrapHelpers::Helpers::NavBarHelper }
  ActiveSupport.on_load(:action_view) { include BootstrapHelpers::Helpers::GridHelper }
  ActiveSupport.on_load(:action_view) { include BootstrapHelpers::Helpers::ErrorHelper }
  ActiveSupport.on_load(:action_view) { include BootstrapHelpers::Helpers::ProgressHelper }
  ActiveSupport.on_load(:action_view) { include BootstrapHelpers::Helpers::ErrorHelper }
  ActiveSupport.on_load(:action_view) { include BootstrapHelpers::Helpers::AlertHelper }
  ActiveSupport.on_load(:action_view) { include BootstrapHelpers::Helpers::IconHelper }
end

There we go. Now when the listen block is called (on file change) not only are the classes redefined, but they are re-included. Now, anytime I change the gem, the objects are refreshed and I can keep on going without having to restart the server.

What’s left for this Ruby on Rails Code?

There are a few things left to address. That will be in a later post, but essentially:

  • I don’t like using string concatenation for paths. File.join or it’s ilk should be used instead.
  • I want to make sure that the ‘reload code’ isn’t firing on production.
  • I need to do some general code cleaning.

I will address these issues in a later post. But I am aware of the issue, and while it does need addressing, this allows me to move forward will less headache.