BLOG
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 Zzeitwerk
s 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 include
d.
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-include
d. 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.