DEV Community

Stan Lo
Stan Lo

Posted on

Replace A Rails Initializer

Rails uses a series of initializers to initialize your application. While some of them might come from the gems you use, most of them are the Rails' built-in ones. And in some rare cases, you might need to customize some of them for special usages.

For example, I had to replace my app's set_routes_reloader_hook initializer (the one that generates routes for your application) to benchmark our route generation logic. And it took a while to figure out how to do it the right way.

So if you don't need to do this, that's totally normal. But if you do, this post is for you.

The Steps

In this post, I'll use the set_routes_reloader_hook initializer as our example. And the replacement will be done in 3 steps:

  1. Identify the Initializer's Source
  2. Register the New Initializer
  3. Drop the Old Initializer

And finally, I'll show you how to reuse the original initializer's body instead of copy&paste the code.

Identify the Initializer's Source

When talking about replacing a Rails initializer, your first thought might be modifying the Rails::Application#initializers directly. But that won't work.

In Rails, Rails::Application#initializers actually consists of 3 initializer sources:

def initializers #:nodoc:
  Bootstrap.initializers_for(self) +
  railties_initializers(super) +
  Finisher.initializers_for(self)
end
Enter fullscreen mode Exit fullscreen mode

So every time you call Rails::Application#initializers, it returns a new array of initializers, and the change made on it won't be applied to the rest of the app.

What we need to do instead, is to identify which source that our target initializer belongs to:

Rails::Application::Bootstrap.initializers_for(Rails.application).map(&:name).include?(:set_routes_reloader_hook) #=> false
Rails::Application.initializers_for(Rails.application).map(&:name).include?(:set_routes_reloader_hook) #=> false
Rails::Application::Finisher.initializers_for(Rails.application).map(&:name).include?(:set_routes_reloader_hook) #=> true
Enter fullscreen mode Exit fullscreen mode

Now we can see that the set_routes_reloader_hook is defined in Rails::Application::Finisher.

Register the New Initializer

Because Rails executes initializers in the order they are defined, we need to make sure the new initializer is placed in the right place. So we need to use the target initializer as our index before dropping it. To do this, we can use the before or after argument when registering the new initializer:

# config/application.rb

require 'rails/all'

Rails::Application::Finisher.initializer :set_routes_reloader_hook_with_benchmark, after: :set_routes_reloader_hook do |app|
  # ....
end
Enter fullscreen mode Exit fullscreen mode

Drop the Old Initializer

And then the last step: drop the target initializer

# config/application.rb

require 'rails/all'

Rails::Application::Finisher.initializer :set_routes_reloader_hook_with_benchmark, after: :set_routes_reloader_hook do |app|
  # ....
end

Rails::Application::Finisher.initializers.reject! { |i| i.name == :set_routes_reloader_hook }
Enter fullscreen mode Exit fullscreen mode

Additional Tip: Reuse the Old Initializer

Since we're "replacing" the initializer instead of just dropping it or adding a new one, it means the new initializer may be very similar to the old one or just an extension of it. But because initializers aren't methods, we can't use super like

def set_routes_reloader_hook
  super
  # other stuff you want to do
end
Enter fullscreen mode Exit fullscreen mode

If this is bothering you, here's a trick: We can extract the old initializer's block, and use instance_eval to evaluate it inside the new initializer


original_initializer_block = Rails::Application::Finisher.initializers.detect { |i| i.name == target_initializer }.block

Rails::Application::Finisher.initializer :new_initializer, after: target_initializer do |app|
  instance_eval(&original_initializer_block)
  # other stuff you want to do
end

Rails::Application::Finisher.initializers.reject! { |i| i.name == target_initializer }
Enter fullscreen mode Exit fullscreen mode

Thank you for reading this post. If you like it, I also wrote some articles about debugging Rails applications/Ruby programs

I'm also working on a gem called tapping_device. It can help you debug Ruby programs more easily by making objects tell you what they do.
Not sure what it means? There are examples in the readme 👇

GitHub logo st0012 / object_tracer

ObjectTracer tracks objects and records their activities

Top comments (2)

Collapse
 
pjmartorell profile image
Pere Joan Martorell

what is the motivation of doing so? I don't see a clear advantage.

Collapse
 
st0012 profile image
Stan Lo • Edited

Hi thanks for the comment! I have just updated post intro accordingly!

My use case was to benchmark our app's route generation, so I had to replace the initializer to insert some benchmark logic.

But this post is not encouraging people to do so, just to provide a way to those who need to do it. Because it took me a while to do it right 😬