I recently had the pleasure of refactoring a view file for a client project. The pleasure part of that sentence is a lie and definitely laced with sarcasm. I grew more grey hairs and thought of throwing my computer out the window several times.
So why was it so damn difficult to make this partial read like a beautifully written poem you got from your high school crush? Too. Much. Logic. There was logic nested within logic smothered in logic. For one thing, it’s hard to test. And for another, no one would be able to know what is going on in this file without staring at it for an hour and contemplating a career switch. Hence why I was refactoring. I shouldn’t be complaining too much, I am working on my refactoring skills and this is the best job I have ever had in my life - but just let me have my time to vent, okay!
Alright, enough emo rambling, let’s get to the meat of the blog.
✨ Decorators and ViewComponents ✨
Ooooh, ahhhhh! Besides feeling spicy on the tongue, what are they? Decorators and ViewComponents help remove logic from a view and make it way more readable and easier to work with. Because the logic is now removed from the view, we can more easily add tests! Yes, I know this adds more work BUT we are living out best practices. And you know what that means: all the gold stars 🤩 ⭐️
Knowing about these tools is important, but let’s dig in a little more so we know how to implement them.
Decorators add behavior to a model instance, or object, before it is passed to the view instead of using helpers. In my head, decorators are like the people who apply makeup and offer advice on wardrobe choices before you go out on stage to have that interview with Jimmy Fallon. As opposed to going on stage and having someone apply makeup and lint-rolling all that dog hair from your button up shirt.
In coding terms, it is a separate ruby class that you can call directly in the view and apply the logic you need. For instance, let’s say you wanted your view to show the name of the user. You can create a decorator called ‘UserDecorator’ and add the method that will contain the logic for your view. For all you visual learners, I got you.
# app/decorators/user_decorator.rb class UserDecorator < SimpleDelegator def full_name [first_name, last_name].join(' ') end end
(I won’t dive deep into Simple delegator since it is a tangent from our current topic but if you want to know more, I found this blog that goes over SimpleDelegator.)
Now that we have UserDecorator containing the logic we need, we can call that decorator and method in the view:
# app/views/users/show.html.erb <%= UserDecorator.new(@user).full_name %>
Boom! Now we have a clean view and it wasn’t even that difficult. If you have a ridiculous amount of logic to separate or think it is too limiting having to couple the logic with a specific model, I have another option for you. A ViewComponent!
A ViewComponent solves the same problems of readability, testing and removal of logic in views. They are Ruby objects that encapsulate a template. ViewComponents are ridiculously easy to test and have a leg up on decorators because they are not tied to a model. Which allows for reusable code to be shared all around.
A ViewComponent is what I used when I refactored the Frankenstein view. It had a TON of logic and I wanted to be able to test out each piece of logic so a ViewComponent was the best option for me. Below is the dumbed down version of one of the ViewComponents I created:
# app/components/trial_banner_component.rb class TrialBannerComponent < ViewComponent::Base def initialize(account:, subscription:) @account = account @subscription = subscription end def render? return false if @subscription&.pending_cancellation? || @subscription&.cancelled? end # a few other methods here that are too much for this example end
Next, I called both of the ViewComponents I created. The logic in each component determined whether or not I rendered one or the other. For example, if the subscription I passed in had a state of "cancelled," the TrialBannerComponent would return false for the render? method. Because render? is false, the component will not render.
# app/components/trial_banner_component.html.erb <%= render(SubscriptionBannerComponent.new(account: current_account, subscription: current_subscription)) %> <%= render(TrialBannerComponent.new(account: current_account, subscription: current_subscription)) %>
My view went from 60 lines of code to 2 lines of code by creating these ViewComponents. The best part is that I was able to create tests for each piece of logic which totaled up to be 9 different tests. 9 instances where we had a difficult time testing and understanding. Now we have confidence and integrity. So yes, I spent a few days switching between refactoring and curling up in the corner crying, BUT I would say the hard work payed off. Special thanks to my co-workers that helped me along the way.
I would highly recommend checking out decorators or ViewComponents the next time you are working with a view that feels too big for its britches. Happy coding everyone!
Psst! We're always looking for people, like you, who are interested in learning and motivated to get better to join our remote team. Get in touch!