Refactoring Rails Helpers from static to dynamic rendering
Tags: rails, ruby, webdev, refactorit
Published: 2024-03-12 13:00:00

Writing code is a journey of continuous improvement, learning, adaptation, and, yes, refactoring. As our applications grow and evolve, we often find ourselves revisiting and rethinking our code to make it more maintainable, scalable, and flexible.

Today, I want to share with you the process of refactoring a helper in a Rails application to dynamically render components. This adventure is filled with twists, turns, and a sprinkle of humour.

When I was explaining this refactoring process to a friend, I realized that it had all the elements of a classic fairy 🧚🏼 tale: a humble beginning, a villain to overcome, a twist in the plot, and a happy ending. So, today I'm trying a new style of writing, a fairy tale, to make it more engaging and fun. So, let's dive in.

#### Chapter One: In the land of static helpers

My story begins with a humble helper, `RapidRailsUI::ViewHelper`, designed to render components in a Rails application. Like any good story, I start small and simple. Here's how it looked initially:

```ruby
module RapidRailsUI
  module ViewHelper
    RAPIDRAILSUI_COMPONENTS = {
      button: "RapidRailsUI::HeadlessButton",
      icon: "RapidRailsUI::IconComponent"
      # Add more components as needed
    }.freeze

    RAPIDRAILSUI_COMPONENTS.each do |name, component|
      define_method :"rui_#{name}" do |*args, **kwargs, &block|
        render component.constantize.new(*args, **kwargs), &block
      end
    end
  end
end
```

This helper served its purpose well 👌🏽, it create a component name such as `rui_buddon` but as the application grew, so did the number of components. And with each new component, I found myself updating the `RAPIDRAILSUI_COMPONENTS` hash. It quickly became apparent that this wasn't the most scalable solution


#### Chapter Two: A small step for a developer, a giant leap for helper kind

To make the helper more dynamic and easier to maintain, I started with a small change. Instead of hardcoding the component names and classes in the `RAPIDRAILSUI_COMPONENTS` hash, I used a symbol array to store the component names. This way, adding new components became a breeze:

```ruby
module RapidRailsUI
  module ViewHelper
    RAPIDRAILSUI_COMPONENTS = %i[button icon].freeze

    RAPIDRAILSUI_COMPONENTS.each do |name|
      define_method :"rui_#{name}" do |*args, **kwargs, &block|
        component_class = "RapidRailsUI::#{name.to_s.classify}Component".constantize
        render component_class.new(*args, **kwargs), &block
      end
    end
  end
end
```

However, this approach still had its limitations. The `RAPIDRAILSUI_COMPONENTS` array still required manual updates for each new component. I needed a more dynamic and flexible solution.

#### Chapter Three: The mystery of the missing dynamism

To address the scalability issue, I embarked on a journey to refactor the view helper to dynamically resolve component names based on the method called. This way, there would be no need to update the helper every time a new component was added. Here's the first iteration of the refactored helper:

```ruby
module RapidRailsUI
  module ViewHelper
    def method_missing(method_name, *args, **kwargs, &block)
      if method_name.to_s.start_with?("rui_")
        component_name = method_name.to_s.sub("rui_", "").classify
        component_class = "RapidRailsUI::#{component_name}Component".safe_constantize
        if component_class
          return render component_class.new(*args, **kwargs), &block
        end
      end

      super
    end

    def respond_to_missing?(method_name, include_private = false)
      method_name.to_s.start_with?("rui_") || super
    end
  end
end
```

With this refactor, the `ViewHelper` became more dynamic and easier to maintain. However, every fairy tale has its villain, and ours was about to make an appearance.


#### Chapter Four: The case of the uncooperative classify

As I happily refactored the components, I faced an unexpected issue with `TabsComponent`. The method `rui_tabs` was resolving to `RapidRailsUI::TabComponent` instead of `RapidRailsUI::TabsComponent`. It turns out that Rails' `classify` method, designed for singularizing model names, was not playing nice with the component names.

To defeat this villain, I introduced a special case in the view helper to handle the pluralization of component names that don't follow the standard Rails convention:

```ruby
module RapidRailsUI
  module ViewHelper
    SPECIAL_PLURALIZATIONS = {
      'tabs' => 'Tabs'
    }.freeze

    def method_missing(method_name, *args, **kwargs, &block)
      if method_name.to_s.start_with?("rui_")
        component_name = method_name.to_s.sub("rui_", "")
        component_name = SPECIAL_PLURALIZATIONS[component_name] || component_name.classify
        component_class = "RapidRailsUI::#{component_name}Component".safe_constantize

        if component_class
          return render component_class.new(*args, **kwargs), &block
        end
      end

      super
    end

    def respond_to_missing?(method_name, include_private = false)
      method_name.to_s.start_with?("rui_") || super
    end
  end
end
```

With this final tweak, `ViewHelper` became both dynamic and accommodating to the special cases. I could now add new components without worrying about updating the helper or dealing with naming conventions.

#### Epilogue: the dawn of a new era in helper land

In the end, the refactoring journey led me to a view helper that was scalable, flexible, and a joy to work with. I no longer had to manually update the helper for each new component, and I could easily handle exceptions to naming conventions.

So, I hope this tale of refactoring has inspired you to embrace the dynamic nature of Ruby and Rails. By leveraging the power of metaprogramming, you can create helpers that are more maintainable, scalable, and delightful to work with.
And they all coded happily ever after. The end.


#### Key Methods Explained

In this refactoring journey, I've used several Ruby and Rails methods to achieve a dynamic and scalable view helper. Let's dive into the details of these key methods:

**`classify`**

- **What it does:** Converts a string to a class name. For example, `"button".classify` becomes `"Button"`.
- **Why it's used:** In the refactor, I used `classify` to dynamically convert component names from strings to class names.
- **Further reading:** [ActiveSupport::Inflector#classify](https://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-classify)

**`safe_constantize`**

- **What it does:** Tries to find a constant with the name specified in the string. If the constant doesn't exist, it returns `nil` instead of raising a `NameError`.
- **Why it's used:** I used `safe_constantize` to safely resolve component class names without risking a crash if the class doesn't exist.
- **Further reading:** [ActiveSupport::Inflector#safe_constantize](https://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-safe_constantize)

**`method_missing`**

- **What it does:** Called when a method is invoked on an object but is not defined for that object. It's a powerful tool for metaprogramming.
- **Why it's used:** I leveraged `method_missing` to dynamically handle method calls for rendering components based on their names starts with `rui_`.
- **Further reading:** [Ruby's `method_missing` documentation](https://ruby-doc.org/core-2.7.0/BasicObject.html#method-i-method_missing)

**`respond_to_missing?`**

- **What it does:** Used in conjunction with `method_missing` to ensure that Ruby's `respond_to?` method works correctly for dynamically handled methods.
- **Why it's used:** I implemented `respond_to_missing?` to accurately report whether the view helper can respond to dynamically generated methods.
- **Further reading:** [Ruby's `respond_to_missing?` documentation](https://ruby-doc.org/core-2.7.0/Object.html#method-i-respond_to_missing-3F)

By understanding these methods, you can harness the power of Ruby and Rails to create more dynamic and flexible code.

## Conclusion
Refactoring view helpers in Rails can be a rewarding adventure. By embracing the dynamic nature of Ruby and Rails, you can transform your helpers into powerful, scalable tools that make your codebase more maintainable and your development experience more enjoyable.

So, the next time you find yourself staring at a helper that needs a little love, remember this tale of refactoring and embark on your own journey of improvement. Your codebase will thank you, and you'll emerge as the hero of your own story.

Happy coding, and may your helpers be ever dynamic and delightful! 🚀