Understanding Presenter Objects vs Direct Rendering in Ruby on Rails

Chetan MittalChetan Mittal
14 min read

In Ruby on Rails applications, developers often face a critical decision about how to handle view-related logic.

Many default to calling #render directly on objects or mixing presentation logic into their models.

This guide explores why this approach is problematic and how presenter objects offer a more maintainable solution helping developers make better architectural decisions for their Rails applications.

The Problem: Direct Rendering and Model Bloat

An examination of common anti-patterns in Rails applications where presentation logic gets mixed with business logic, leading to maintenance difficulties and potential security vulnerabilities.

Common but Problematic Approaches

Examples of frequently seen but problematic practices that compromise code quality and maintainability in Rails applications.

# In your view
<%= @user.render %>
# or
<%= render @product %>

# In your model (app/models/user.rb)
class User < ApplicationRecord
  def full_name_display
    "<h2>#{first_name} #{last_name}</h2>".html_safe
  end

  def status_badge
    if active?
      '<span class="badge success">Active</span>'.html_safe
    else
      '<span class="badge danger">Inactive</span>'.html_safe
    end
  end

  def formatted_join_date
    created_at.strftime("%B %d, %Y")
  end
end

Why This Is Problematic

  1. Safety Issues:

    • Direct HTML generation in models is dangerous - Creates potential security vulnerabilities by allowing uncontrolled HTML injection into views

    • Unsafe usage of html_safe - Bypasses Rails' automatic HTML escaping, potentially allowing malicious code execution

    • Potential for XSS vulnerabilities - Increases risk of cross-site scripting attacks through unsanitized user input

  2. Separation of Concerns:

    • Models contain presentation logic - Violates single responsibility principle by mixing data management with display code

    • Business logic gets mixed with display code - Creates confusion about where specific functionality should reside

    • Views become tightly coupled to model implementations - Makes it difficult to modify either views or models independently

  3. Maintenance Challenges:

    • Changes to display require model modifications - Forces developers to update core business logic when only presentation needs change

    • Testing becomes more complex - Requires testing both business logic and presentation logic in the same test suite

    • Code reuse is difficult - Makes it challenging to share presentation logic across different views or applications

The Solution: Presenter Objects

Presenters are a design pattern that acts as an intermediary layer between your models and views, specifically handling presentation logic.

A design pattern that introduces a dedicated layer for handling view-related logic, providing better separation of concerns and improved maintainability while keeping models focused on business logic.

Think of them as specialized objects that take your raw data and dress it up for display, similar to how a store window dresser takes merchandise and presents it in the most appealing way possible.

Key Characteristics of Presenters

The core features that make presenters effective at handling view logic while maintaining clean code.

Decoration Pattern

Presenters wrap around existing objects (typically models) to add presentation-specific methods without modifying the original object. Like putting a beautiful frame around a painting, it enhances presentation without changing the original work.

  • Wraps existing objects - Adds presentation functionality without modifying the original object structure

  • Enhances presentation - Provides specialized methods specifically for display purposes

  • Preserves original object - Maintains clean separation between business and presentation logic

View-Specific Logic

Presenters handle formatting, display conditions, and HTML generation that would otherwise clutter models or views. This is similar to how a news editor takes raw facts and formats them into a readable article.

  • Handles formatting - Converts raw data into human-readable formats

  • Manages display conditions - Controls when and how different pieces of data are shown

  • Generates HTML safely - Creates properly escaped and structured HTML output

Delegation

Presenters typically delegate basic attributes to the underlying object while adding presentation-specific methods. Think of it as a spokesperson who can relay basic information but also adds context and polish to the message.

  • Forwards basic attributes - Provides transparent access to underlying object properties

  • Adds presentation methods - Extends functionality with view-specific display methods

  • Maintains clean interface - Keeps interaction with presenter objects intuitive and consistent

Code Example

# Basic presenter implementation showing key characteristics
class UserPresenter < ApplicationPresenter
  # Delegation - forwarding basic attributes to the model
  delegate :email, :username, to: :object

  # View-specific logic
  def display_name
    if object.full_name.present?
      h.content_tag(:span, object.full_name, class: 'full-name')
    else
      h.content_tag(:span, username, class: 'username')
    end
  end

  # Complex formatting
  def member_since
    h.content_tag(:div, class: 'member-info') do
      "Member since: #{h.l(object.created_at, format: :long)}"
    end
  end
end

# Usage in view
<%= @user_presenter.display_name %>
<%= @user_presenter.member_since %>

Common Use Cases of Presenters

The typical scenarios where presenters really shine in your Rails applications.

Complex Formatting

Handles tricky formatting like dates, currencies, and status messages in a clean, reusable way.

  • Date/time formatting: Converting "2024-03-21" to "March 21st, 2024"

  • Status display logic: Transforming boolean flags into user-friendly messages

  • Currency formatting: Converting 1000 to "$1,000.00"

  • Name concatenation: Combining "John" and "Doe" into "Mr. John Doe"

Conditional Display Logic

Manages what gets shown to whom based on permissions and state - perfect for dynamic UIs.

  • Permission-based content: Showing admin controls only to administrators

  • State-dependent UI elements: Displaying different badges based on order status

  • Context-specific formatting: Showing prices with or without tax based on region

HTML Generation

Creates HTML safely and consistently, preventing security issues while keeping your code DRY.

  • Safe HTML content creation: Generating HTML without security vulnerabilities

  • Complex markup structures: Creating nested elements with proper attributes

  • Reusable UI components: Building consistent interface elements

Code Example

class ProductPresenter < ApplicationPresenter
  # Complex Formatting Example
  def price_display
    h.number_to_currency(object.price, precision: 2)
  end

  # Conditional Display Logic Example
  def stock_status
    if object.in_stock?
      h.content_tag(:span, "In Stock", class: "text-green")
    else
      h.content_tag(:span, "Out of Stock", class: "text-red")
    end
  end

  # HTML Generation Example
  def product_card
    h.content_tag(:div, class: "product-card") do
      h.concat h.image_tag(object.image_url, class: "product-image")
      h.concat h.content_tag(:h3, object.name)
      h.concat price_display
      h.concat stock_status
    end
  end
end

Presenters vs ViewComponents vs Phlex

Presenters

A lightweight solution for handling view-related logic without additional dependencies.

Advantages

  • Lightweight and simple to implement: Easy to add to existing Rails applications.

  • No additional dependencies: Works with standard Rails, no extra gems needed.

  • Easy to understand and maintain: Follows familiar Ruby patterns.

  • Great for basic view logic extraction: Perfect for simple formatting needs and display logic.

Disadvantages

  • No template encapsulation: Views and presenters remain separate.

  • Can lead to large presenter classes: May become unwieldy as complexity grows.

  • Limited re-usability across different contexts: Not as flexible as modern component systems.

Code Example

# app/presenters/order_presenter.rb
class OrderPresenter < ApplicationPresenter
  def total_amount
    h.number_to_currency(object.total)
  end

  def status_badge
    h.content_tag(:span, object.status.titleize, class: "badge #{status_color}")
  end

  private

  def status_color
    case object.status
    when 'pending' then 'yellow'
    when 'completed' then 'green'
    when 'cancelled' then 'red'
    end
  end
end

# Usage in view
<div class="order-summary">
  <h2>Order #<%= @order_presenter.object.id %></h2>
  <p>Total: <%= @order_presenter.total_amount %></p>
  <p>Status: <%= @order_presenter.status_badge %></p>
</div>

ViewComponents

ViewComponents, created by GitHub, bring a React-like component approach to Rails. They represent a more structured way to build reusable interface elements.

Advantages

  • Full template encapsulation: HTML and Ruby code live together

  • Better testing capabilities: Specialized testing tools and patterns

  • More structured approach to UI components: Clear organization and conventions

  • Great for reusable UI elements: Perfect for design systems

Disadvantages

  • Additional dependency: Requires adding the view_component gem

  • More complex setup: Need to configure and structure components

  • Steeper learning curve: New concepts to learn

  • Might be overkill for simple presentation needs: Too structured for basic formatting

Code Example

# app/components/order_summary_component.rb
class OrderSummaryComponent < ViewComponent::Base
  def initialize(order:)
    @order = order
  end

  private

  attr_reader :order
end

# app/components/order_summary_component.html.erb
<div class="order-summary">
  <h2>Order #<%= order.id %></h2>
  <p>Total: <%= number_to_currency(order.total) %></p>
  <span class="badge <%= status_color %>">
    <%= order.status.titleize %>
  </span>
</div>

# Usage in view
<%= render(OrderSummaryComponent.new(order: @order)) %>

Phlex

Phlex offers a modern, Ruby-first approach to writing views. It's like writing HTML in Ruby, with added type safety and performance benefits.

Advantages

  • Type-safe views: Catch errors before runtime

  • Better performance than ERB: More efficient rendering

  • Pure Ruby syntax: No context switching between Ruby and HTML

  • Great IDE support: Better autocomplete and error checking

Disadvantages

  • Newest solution with less community support: Fewer resources and examples

  • Different syntax paradigm: Takes time to adjust to Ruby-based templates

  • May not fit traditional Rails workflows: Different from conventional ERB

  • Learning curve for team members: New way of thinking about views

Code Example

# app/views/order_summary_view.rb
class OrderSummaryView < Phlex::HTML
  def initialize(order:)
    @order = order
  end

  def template
    div(class: "order-summary") do
      h2 { "Order ##{@order.id}" }
      p { "Total: #{number_to_currency(@order.total)}" }
      span(class: ["badge", status_color]) { @order.status.titleize }
    end
  end

  private

  def status_color
    case @order.status
    when 'pending' then 'yellow'
    when 'completed' then 'green'
    when 'cancelled' then 'red'
    end
  end
end

# Usage in view
<%= render OrderSummaryView.new(order: @order) %>

Which One to use When?

  1. Use Presenters When:

    • Simple view logic extraction is needed: Basic formatting and display logic

    • Working with legacy applications: Easy to integrate without major changes

    • Quick implementation is priority: Get up and running fast

    • Team is familiar with traditional Rails patterns: No new concepts to learn

  2. Use ViewComponents When:

    • Building reusable UI components: Design system elements

    • Need strong testing infrastructure: Component-specific tests

    • Working on larger applications: Better organization for complex UIs

    • Want template encapsulation: Keep related code together

  3. Use Phlex When:

    • Performance is critical: Need fastest possible rendering

    • Want type safety: Catch errors early

    • Prefer pure Ruby views: Avoid template syntax

    • Building new applications with modern practices: Start fresh with latest tools

How to Integrate Presenters with Components?

  1. Performance Impact:

    • Presenters: Like a lightweight wrapper, minimal impact on performance

    • ViewComponents: Small overhead from component initialization

    • Phlex: Optimized for speed, can be faster than traditional templates

  2. Development Workflow:

    • Presenters: Fits naturally into Rails MVC pattern

    • ViewComponents: Component-based development like modern frontend

    • Phlex: Ruby-first approach to templates

  3. Testing Approach:

    • Presenters: Standard unit tests for formatting logic

    • ViewComponents: Specialized component testing tools

    • Phlex: Ruby-based view testing with type checking

Code Example

# Using Presenter with ViewComponent
class OrderComponent < ViewComponent::Base
  def initialize(order:)
    @presenter = OrderPresenter.new(order)
  end

  private

  attr_reader :presenter
end

# order_component.html.erb
<div class="order-component">
  <%= presenter.total_amount %>
  <%= presenter.status_badge %>
</div>

# Using Presenter with Phlex
class OrderView < Phlex::HTML
  def initialize(order:)
    @presenter = OrderPresenter.new(order)
  end

  def template
    div(class: "order-view") do
      raw(@presenter.total_amount)
      raw(@presenter.status_badge)
    end
  end
end

How to Setup the Presenter Infrastructure?

Creating a Base Presenter

Creates a foundation for all presenter objects with common functionality. Let’s see the code example as below:-

# app/presenters/application_presenter.rb
class ApplicationPresenter
  include ActionView::Helpers::TextHelper
  include ActionView::Helpers::TagHelper
  include ActionView::Helpers::OutputSafetyHelper

  def initialize(object, view_context = nil)
    @object = object
    @view_context = view_context
  end

  private

  attr_reader :object, :view_context
  alias_method :h, :view_context
end

Implementing a User Presenter

Example of a concrete presenter implementation showing common patterns.

# app/presenters/user_presenter.rb
class UserPresenter < ApplicationPresenter
  delegate :first_name, :last_name, :active?, to: :object

  def full_name_display
    h.content_tag :h2, full_name
  end

  def status_badge
    h.content_tag :span, status_text, class: status_classes
  end

  def formatted_join_date
    h.l(object.created_at, format: :long)
  end

  private

  def full_name
    "#{first_name} #{last_name}"
  end

  def status_text
    active? ? "Active" : "Inactive"
  end

  def status_classes
    "badge #{active? ? 'success' : 'danger'}"
  end
end

Integrating with Controller

Shows how to integrate presenters into Rails controllers.

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def show
    user = User.find(params[:id])
    @user_presenter = UserPresenter.new(user, view_context)
  end
end

Implementing the Clean View

Demonstrates clean view code using presenters.

<%# app/views/users/show.html.erb %>
<div class="user-profile">
  <%= @user_presenter.full_name_display %>
  <div class="status">
    <%= @user_presenter.status_badge %>
  </div>
  <p>Member since: <%= @user_presenter.formatted_join_date %></p>
</div>

Let’s Explore a Few Advanced Presenter Patterns

Collection Presenters

Handles presenting collections of objects efficiently.

# app/presenters/user_collection_presenter.rb
class UserCollectionPresenter
  include Enumerable

  def initialize(users, view_context = nil)
    @users = users
    @view_context = view_context
  end

  def each(&block)
    @users.each do |user|
      block.call(UserPresenter.new(user, @view_context))
    end
  end

  def active_users_count
    @users.count(&:active?)
  end

  def grouped_by_status
    @users.group_by(&:active?).transform_values do |users|
      self.class.new(users, @view_context)
    end
  end
end

Contextual Presenters

Creates specialized presenters for different contexts.

# app/presenters/admin/user_presenter.rb
module Admin
  class UserPresenter < ::UserPresenter
    def admin_controls
      return unless h.current_user.admin?

      h.content_tag :div, class: 'admin-controls' do
        h.concat h.link_to('Edit', h.edit_admin_user_path(object))
        h.concat h.link_to('Delete', h.admin_user_path(object), 
                          method: :delete,
                          data: { confirm: 'Are you sure?' })
      end
    end
  end
end

How to Test Presenters?

Demonstrates effective testing strategies for presenters.

Code Example

# spec/presenters/user_presenter_spec.rb
RSpec.describe UserPresenter do
  let(:user) { create(:user, first_name: "John", last_name: "Doe") }
  let(:view_context) { ActionController::Base.new.view_context }
  let(:presenter) { described_class.new(user, view_context) }

  describe "#full_name_display" do
    it "wraps the full name in an h2 tag" do
      expect(presenter.full_name_display).to have_selector("h2")
      expect(presenter.full_name_display).to have_content("John Doe")
    end
  end

  describe "#status_badge" do
    context "when user is active" do
      before { user.update(active: true) }

      it "displays correct badge" do
        expect(presenter.status_badge).to have_selector("span.badge.success")
        expect(presenter.status_badge).to have_content("Active")
      end
    end
  end
end

Best Practices for Implementing Presenters

Clear Responsibility Separation

Maintains clean separation between business and presentation logic.

# Bad - Mixing concerns
class ProductPresenter < ApplicationPresenter
  def calculate_discount
    object.price * 0.8  # Business logic in presenter
  end
end

# Good - Presentation only
class ProductPresenter < ApplicationPresenter
  def formatted_discount
    h.number_to_currency(object.calculated_discount)
  end
end

Safe HTML Generation

Ensures secure HTML creation without vulnerabilities.

# Bad
def status_html
  "<span class='#{status_class}'>#{status}</span>".html_safe
end

# Good
def status_html
  h.content_tag(:span, status, class: status_class)
end

View Context Usage

Properly utilizes Rails view helpers and routing.

# Bad
def profile_link
  "<a href='/users/#{object.id}'>#{full_name}</a>".html_safe
end

# Good
def profile_link
  h.link_to(full_name, h.user_path(object))
end

When to Use Presenters?

Use presenters when:

  1. You find HTML generation in your models

  2. Views contain complex formatting logic

  3. You need different presentations of the same data

  4. You want to make your views more testable

  5. You need to reuse presentation logic across views

Conclusion

Moving away from direct rendering and adopting presenter objects offers several benefits:

  • Cleaner, more maintainable code

  • Better separation of concerns

  • Improved testing capability

  • More reusable presentation logic

  • Safer HTML generation

Remember: The goal isn't just to move code around—it's to create a more maintainable and secure application architecture.

Presenters provide a structured way to handle view-specific logic while keeping your models focused on business concerns.

Bonus: Presenters vs Decorators

While presenters and decorators might seem similar at first glance, they serve different purposes in Rails applications.

Understanding these differences helps in choosing the right pattern for your specific needs.

Core Differences

  1. Primary Purpose:

    • Presenters: Focus specifically on view-layer presentation logic

    • Decorators: Enhance objects with additional behavior, not necessarily view-related

  2. Scope:

    • Presenters: Strictly handle view-specific formatting and display logic

    • Decorators: Can add any type of behavior (business logic, data transformation, etc.)

  3. Context Awareness:

    • Presenters: Usually aware of view context (helpers, routes, etc.)

    • Decorators: Generally context-independent

Implementation Examples

Decorator Pattern Example

# Basic Decorator using SimpleDelegator
require 'delegate'

class UserDecorator < SimpleDelegator
  def full_name
    "#{first_name} #{last_name}"
  end

  def age_in_years
    ((Time.current - birthdate.to_time) / 1.year).floor
  end

  def premium_user?
    subscriptions.any?(&:active?) && created_at < 1.year.ago
  end
end

# Usage
user = User.find(1)
decorated_user = UserDecorator.new(user)
decorated_user.full_name       # => "John Doe"
decorated_user.age_in_years    # => 25

Presenter Pattern Example

class UserPresenter < ApplicationPresenter
  include ActionView::Helpers::DateHelper

  def full_name_display
    h.content_tag(:h2, "#{object.first_name} #{object.last_name}", 
      class: name_class)
  end

  def age_display
    h.content_tag(:span, "#{age_in_years} years old", 
      class: 'user-age')
  end

  def membership_badge
    return unless object.premium_user?

    h.content_tag(:div, class: 'premium-badge') do
      h.image_tag('premium-star.png') +
      h.content_tag(:span, 'Premium Member')
    end
  end

  private

  def name_class
    object.premium_user? ? 'premium-name' : 'regular-name'
  end

  def age_in_years
    ((Time.current - object.birthdate.to_time) / 1.year).floor
  end
end

# Usage
user = User.find(1)
presenter = UserPresenter.new(user, view_context)
<%= presenter.full_name_display %>
<%= presenter.age_display %>
<%= presenter.membership_badge %>

Combined Usage Example

Sometimes, you might want to use both presenters and decorators together:

# Decorator for business logic enhancements
class UserDecorator < SimpleDelegator
  def premium_user?
    subscriptions.any?(&:active?) && created_at < 1.year.ago
  end

  def total_spending
    orders.completed.sum(:total_amount)
  end

  def loyalty_tier
    case total_spending
    when 0..999 then :bronze
    when 1000..4999 then :silver
    else :gold
    end
  end
end

# Presenter for view-specific formatting
class UserPresenter < ApplicationPresenter
  def loyalty_badge
    h.content_tag(:div, class: "badge #{object.loyalty_tier}") do
      h.concat h.image_tag("#{object.loyalty_tier}-badge.png")
      h.concat loyalty_text
    end
  end

  def spending_summary
    h.content_tag(:div, class: 'spending-info') do
      h.concat h.content_tag(:span, 'Total Spent: ')
      h.concat h.number_to_currency(object.total_spending)
    end
  end

  private

  def loyalty_text
    "#{object.loyalty_tier.to_s.titleize} Member"
  end
end

# Usage in controller
class UsersController < ApplicationController
  def show
    user = User.find(params[:id])
    decorated_user = UserDecorator.new(user)
    @user_presenter = UserPresenter.new(decorated_user, view_context)
  end
end

# Usage in view
<div class="user-profile">
  <%= @user_presenter.loyalty_badge %>
  <%= @user_presenter.spending_summary %>
</div>

When to Use Which?

Use Decorators When:

  1. Adding Business Logic:

     class OrderDecorator < SimpleDelegator
       def refundable?
         created_at > 30.days.ago && !refunded? && status == 'completed'
       end
     end
    
  2. Enhancing Data Access:

     class ArticleDecorator < SimpleDelegator
       def related_articles
         Article.published
               .where(category: category)
               .where.not(id: id)
               .limit(3)
       end
     end
    
  3. Adding Model-Level Behavior:

     class ProductDecorator < SimpleDelegator
       def discounted_price
         return price unless on_sale?
         price * (1 - discount_percentage)
       end
     end
    

Use Presenters When:

  1. Formatting for Display:

     class ProductPresenter < ApplicationPresenter
       def price_display
         if object.on_sale?
           h.content_tag(:div, class: 'price-block') do
             h.concat h.content_tag(:span, original_price, class: 'original-price')
             h.concat h.content_tag(:span, sale_price, class: 'sale-price')
           end
         else
           h.number_to_currency(object.price)
         end
       end
     end
    
  2. HTML Generation:

     class CommentPresenter < ApplicationPresenter
       def formatted_content
         h.content_tag(:div, class: 'comment') do
           h.concat author_avatar
           h.concat comment_body
           h.concat timestamp
         end
       end
     end
    
  3. View-Specific Logic:

     class OrderPresenter < ApplicationPresenter
       def status_label
         h.content_tag(:span, object.status.titleize, 
           class: "status-label #{status_class}")
       end
     end
    

Key Takeaways

Essential points for choosing between presenters and decorators:

  1. Separation of Concerns:

    • Decorators enhance objects with business logic

    • Presenters handle view-specific formatting

  2. Context Requirements:

    • Decorators work independently

    • Presenters need view context

  3. Testing Approach:

    • Decorators: Focus on business logic

    • Presenters: Focus on HTML generation and formatting

  4. Maintenance:

    • Decorators: Update when business rules change

    • Presenters: Update when UI requirements change

0
Subscribe to my newsletter

Read articles from Chetan Mittal directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Chetan Mittal
Chetan Mittal

I stumbled upon Ruby on Rails beta version in 2005 and has been using it since then. I have also trained multiple Rails developers all over the globe. Currently, providing consulting and advising companies on how to upgrade, secure, optimize, monitor, modernize, and scale their Rails apps.