Rails View Components

Ruby on Rails is a framework that delivers a tremendous amount of developer productivity and happiness. Unsurprisingly, Rails application also go through growing pains as they mature. Models and controllers expand until small objects are extracted to keep them under control. The same happens with Rails views; they start out powerful and easy to use and slowly grow out of control. The views become hard to reason about and maintain. Views are also inherently hard to test, so they become the riskiest part of a Rails application.

Decorator

Decorators are added to contain the situation. Gems exist to help with this, but we don’t need them. SimpleDelegator is built into Ruby so we can make use of it like so:

class CommentDecorator < SimpleDelegator
  def gravatar
  end

  def timestamp
  end

  def author_name
  end
end

CommentDecorator.new(comment)

Decorators are great, and they help for a while. The problem is that they are tightly coupled to models. Models tend to describe big ideas in the system that are displayed in many different ways. Decorators, therefore, get bloated as they start describing numerous UIs.

View Component

Applying basic OOP principles we can break a decorator into smaller objects. Instead of a CommentDecorator we can build several components: CommentForm, CommentBox, CommentThread, ReactionButtons, etc. We don’t need a fancy gem to do this. A plain old ruby object will do:

class ViewComponent
  include ActiveModel::Model
  attr_accessor :context

  def render
    context.render(
      partial: "components/#{template_path}",
      locals: { component: self }
    )
  end

  private
  def template_path
    self.class.to_s.underscore
  end
end

The view component objects can be put under app/view_components and their templates can be placed in app/views/components/. The template is just a rails partial that gets a component local variable.

The view components will each inherit from the main ViewComponent class:

# object at app/view_components/comment_box.rb
# template at app/views/components/_comment_box.html.erb
class CommentBox < ViewComponent
end

# object at app/view_components/comment_thread.rb
# template at app/views/components/_comment_thread.html.erb
class CommentThread < ViewComponent
end

# object at app/view_components/comment_form.rb
# template at app/views/components/_comment_form.html.erb
class CommentForm < ViewComponent
end

We’ll add a helper to facilitate the rendering of a view component:

def render_component(component, props)
  component.new({ context: self }.merge(props)).render
end

Now anywhere in our views we can render a component with:

<%= render_component(CommentForm, {some: 'property'}) %>

View components are ideally minimal as they have a single and focused responsibility. This makes them easy to test and reason about. We can use a real rendering context if we wanted to in tests or mock it and do isolation testing.

Interactivity

View components implemented in Ruby are a good solution until interactivity is required. If for example, our user interfaces need to respond to mouse events then the Ruby classes become a dead-end. Vue.js or Backbone.js are excellent JavaScript libraries for adding interactivity on-top of server-rendered views. To pass properties from the ruby side to the JavaScript side, we just have to make sure they exist in the HTML data attribute:

<%= tag.div id: "element-id", data: { name: "Ahmed" }.to_json do %>
  <p>Component template here!</p>
<% end %>
const node = document.getElementById('element-id');
const props = JSON.parse(node.getAttribute('data'));

const component = new Vue({
  el: '#element-id',
  data: props
});

This is, in my opinion, a great solution for applications that just need a sprinkle of interactivity. Of course a component that is spread over four languages (Ruby, JavaScript, HTML, and CSS) becomes harder to test and contribute to. For new projects I recommend switching entirely to front-end components. Backbone.js, React, or Vue.js can be used to build both the template and behavior of complex UI components. This is a topic I want to explore more in the future on this blog.