Understanding Presenter Objects vs Direct Rendering in Ruby on Rails

Table of contents
- The Problem: Direct Rendering and Model Bloat
- The Solution: Presenter Objects
- Key Characteristics of Presenters
- Common Use Cases of Presenters
- Presenters vs ViewComponents vs Phlex
- How to Setup the Presenter Infrastructure?
- Let’s Explore a Few Advanced Presenter Patterns
- How to Test Presenters?
- Best Practices for Implementing Presenters
- When to Use Presenters?
- Conclusion
- Bonus: Presenters vs Decorators

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
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
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
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?
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
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
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?
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
Development Workflow:
Presenters: Fits naturally into Rails MVC pattern
ViewComponents: Component-based development like modern frontend
Phlex: Ruby-first approach to templates
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:
You find HTML generation in your models
Views contain complex formatting logic
You need different presentations of the same data
You want to make your views more testable
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
Primary Purpose:
Presenters: Focus specifically on view-layer presentation logic
Decorators: Enhance objects with additional behavior, not necessarily view-related
Scope:
Presenters: Strictly handle view-specific formatting and display logic
Decorators: Can add any type of behavior (business logic, data transformation, etc.)
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:
Adding Business Logic:
class OrderDecorator < SimpleDelegator def refundable? created_at > 30.days.ago && !refunded? && status == 'completed' end end
Enhancing Data Access:
class ArticleDecorator < SimpleDelegator def related_articles Article.published .where(category: category) .where.not(id: id) .limit(3) end end
Adding Model-Level Behavior:
class ProductDecorator < SimpleDelegator def discounted_price return price unless on_sale? price * (1 - discount_percentage) end end
Use Presenters When:
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
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
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:
Separation of Concerns:
Decorators enhance objects with business logic
Presenters handle view-specific formatting
Context Requirements:
Decorators work independently
Presenters need view context
Testing Approach:
Decorators: Focus on business logic
Presenters: Focus on HTML generation and formatting
Maintenance:
Decorators: Update when business rules change
Presenters: Update when UI requirements change
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.