Rails View Patterns: Helpers vs Partials vs Presenters vs Decorators
Table of contents
- Understanding the Core Patterns
- Helpers: Your First Line of Defense
- Partials: Component-Based View Organization
- Presenters: Managing Complex View Logic
- Decorators: Enhancing Objects with Additional Behavior
- Decorators vs Presenters
- Alternative Approach: View Components
- Best Practices and Guidelines
- Making the Right Choice
- Real-World Example: Complex Product Listing
- Conclusion
As Ruby on Rails applications grow in complexity, maintaining clean and organized views becomes increasingly challenging.
Rails provides several patterns to help manage this complexity: Helpers, Partials, Presenters, and Decorators.
Each serves a specific purpose and, when used appropriately, can significantly improve code organization and maintainability.
In this comprehensive guide, we'll explore when and how to use each pattern effectively.
Understanding the Core Patterns
Before diving deep into specific use cases, let's establish a clear understanding of each pattern:
Helpers: Module-based utility methods for view-related logic
Partials: Reusable view templates for HTML fragments
Presenters: Objects that encapsulate complex view logic involving multiple models
Decorators: Objects that enhance or modify the behavior of individual objects through composition, allowing for dynamic addition of responsibilities
Helpers: Your First Line of Defense
Helpers are the simplest and most straightforward way to extract reusable view logic. They're particularly useful for small, focused utilities that don't require complex object interactions.
When to Use Helpers
Format data consistently across views
Generate common HTML structures
Create reusable UI components
Handle simple conditional logic
Wrap Rails built-in helpers with application-specific defaults
Let's look at a practical example:
# app/helpers/application_helper.rb
module ApplicationHelper
def format_price(amount, currency = '€')
return 'Free' if amount.zero?
formatted = number_to_currency(amount, unit: currency,
delimiter: ',',
separator: '.')
content_tag(:span, formatted, class: 'price')
end
end
This helper demonstrates several best practices:
Handles a specific presentation concern (price formatting)
Provides reasonable defaults
Combines multiple Rails helpers (number_to_currency and content_tag)
Includes business logic (showing 'Free' for zero amounts)
Partials: Component-Based View Organization
Partials excel at breaking down complex views into manageable, reusable pieces. They're particularly valuable when dealing with repeated UI elements or when organizing large templates into logical sections.
When to Use Partials
Extract repeated view fragments
Break down complex views into manageable chunks
Share common UI elements across different views
Create reusable form templates
Implement sidebar elements, headers, or footers
Here's an example of effective partial usage:
# app/views/products/show.html.erb
<div class="product-page">
<%= render 'shared/header' %>
<div class="product-details">
<%= render 'product_info', product: @product %>
<div class="related-items">
<%= render partial: 'product',
collection: @related_products,
locals: { show_price: true } %>
</div>
</div>
<%= render 'shared/footer' %>
</div>
# app/views/products/_product_info.html.erb
<div class="product-info">
<h1><%= product.name %></h1>
<div class="price">
<%= format_price(product.price) %>
</div>
<%= render 'products/availability', product: product %>
</div>
This example shows how to:
Use shared partials for common elements
Pass local variables to partials
Render collections efficiently
Nest partials for complex UI components
Presenters: Managing Complex View Logic
Presenters shine when dealing with complex view logic that involves multiple models or complicated calculations. They help keep controllers and views slim while providing a dedicated home for view-specific business logic.
When to Use Presenters
Complex calculations involving multiple models
View-specific data transformations
Complex conditional logic
Dashboard-style data aggregation
Charts and graphs data preparation
Here's a practical presenter implementation:
# app/presenters/dashboard_presenter.rb
class DashboardPresenter
def initialize(user)
@user = user
end
def recent_activity
{
comments: recent_comments,
posts: recent_posts,
interactions: recent_interactions
}
end
def engagement_stats
{
total_posts: @user.posts.count,
total_comments: @user.comments.count,
engagement_rate: calculate_engagement_rate,
trending_topics: get_trending_topics
}
end
private
def recent_comments
@user.comments.includes(:post)
.order(created_at: :desc)
.limit(5)
end
def recent_posts
@user.posts.includes(:category)
.order(created_at: :desc)
.limit(5)
end
def recent_interactions
@user.interactions.includes(:interactable)
.order(created_at: :desc)
.limit(10)
end
def calculate_engagement_rate
return 0 if @user.posts.empty?
total_interactions = @user.posts.sum(:interactions_count)
(total_interactions.to_f / @user.posts.count).round(2)
end
def get_trending_topics
@user.posts.joins(:tags)
.group('tags.name')
.order('count_all DESC')
.limit(5)
.count
end
end
# app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
def show
@presenter = DashboardPresenter.new(current_user)
end
end
# app/views/dashboard/show.html.erb
<div class="dashboard">
<% activity = @presenter.recent_activity %>
<div class="recent-activity">
<%= render 'recent_comments', comments: activity[:comments] %>
<%= render 'recent_posts', posts: activity[:posts] %>
<%= render 'recent_interactions',
interactions: activity[:interactions] %>
</div>
<div class="stats">
<%= render 'engagement_stats',
stats: @presenter.engagement_stats %>
</div>
</div>
This presenter example demonstrates:
Encapsulation of complex data gathering
Clean separation of concerns
Efficient database queries
Logical organization of related methods
Decorators: Enhancing Objects with Additional Behavior
Decorators add functionality to objects through composition, allowing you to extend their behavior without inheritance. Unlike Presenters, which focus specifically on view-layer concerns, Decorators can enhance objects with any type of additional behavior.
When to Use Decorators
Add computational methods to models
Transform or enhance data
Add business logic that doesn't belong in the model
Implement cross-cutting concerns
Create specialized versions of objects
Here's an example using Ruby's SimpleDelegator:
# app/decorators/user_decorator.rb
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
def total_spending
orders.completed.sum(:total_amount)
end
def preferred_categories
ordered_products
.group(:category)
.order('count_all DESC')
.limit(3)
.count
end
end
# Usage in a service object
class UserAnalyticsService
def initialize(user)
@user = UserDecorator.new(user)
end
def generate_insights
{
spending_level: spending_tier,
account_status: account_health,
recommendations: generate_recommendations
}
end
private
def spending_tier
case @user.total_spending
when 0..100 then :bronze
when 101..500 then :silver
else :gold
end
end
def account_health
return :excellent if @user.premium_user?
return :good if @user.age_in_years > 1
:new
end
def generate_recommendations
RecommendationEngine.new(@user.preferred_categories).suggest
end
end
This decorator example shows:
Context-independent behavior enhancement
Business logic additions
Data transformation methods
Integration with other services
Decorators vs Presenters
To clarify the distinction between Decorators and Presenters:
Decorator Characteristics
Enhances objects with any type of behavior
Context-independent
Can include business logic
Uses delegation to extend functionality
Often used outside of views
Presenter Characteristics
Focuses specifically on view-layer presentation
View-context aware (has access to helpers, routes)
Handles only display and formatting logic
May work with multiple models
Used exclusively in view context
Here's a contrasting example of a Presenter to highlight the differences:
# app/presenters/user_profile_presenter.rb
class UserProfilePresenter
include ActionView::Helpers::TextHelper
include ActionView::Helpers::DateHelper
def initialize(user, view_context)
@user = user
@view_context = view_context
end
def display_name
h.content_tag :h1, @user.full_name, class: 'profile-name'
end
def membership_status
h.content_tag :div, class: "status #{@user.status}" do
"Member for #{time_ago_in_words(@user.created_at)}"
end
end
def activity_summary
recent_activities = @user.activities.recent
h.render 'shared/activity_list',
activities: recent_activities,
highlight_new: true
end
private
def h
@view_context
end
end
Alternative Approach: View Components
While not part of the traditional Rails patterns, View Components offer a modern approach to view organization that combines the best aspects of partials and presenters.
# app/components/product_card_component.rb
class ProductCardComponent < ViewComponent::Base
def initialize(product:, show_actions: true)
@product = product.decorate
@show_actions = show_actions
end
def cache_key
[@product, @show_actions]
end
private
def stock_status
return 'Out of Stock' if @product.out_of_stock?
"#{@product.stock_count} in stock"
end
def show_purchase_button?
@show_actions && @product.available_for_purchase?
end
end
# app/components/product_card_component.html.erb
<div class="product-card" data-test="product-<%= @product.id %>">
<div class="product-image">
<%= image_tag @product.featured_image_url %>
<%= render BadgeComponent.new(text: stock_status) %>
</div>
<div class="product-details">
<h3><%= @product.name %></h3>
<p class="description"><%= @product.truncated_description %></p>
<div class="pricing">
<%= @product.formatted_price %>
<% if @product.on_sale? %>
<span class="sale-badge">On Sale</span>
<% end %>
</div>
<% if show_purchase_button? %>
<%= render ButtonComponent.new(
text: "Add to Cart",
action: :add_to_cart,
product_id: @product.id
) %>
<% end %>
</div>
</div>
This View Component example demonstrates:
Encapsulated logic and template
Clear initialization interface
Caching support
Composition with other components
Best Practices and Guidelines
To effectively use these patterns, consider the following guidelines:
For Helpers
Keep methods focused and single-purpose
Avoid complex business logic
Use for truly reusable functionality
Document complex helper methods
Test thoroughly, as helpers are global
For Partials
Use meaningful names that indicate purpose
Keep partials focused on one aspect of the UI
Use locals over instance variables
Consider caching for expensive partials
Document required locals
For Presenters
Focus on data preparation and transformation
Avoid modifying application state
Use for complex multi-model interactions
Consider caching expensive calculations
Test thoroughly, including edge cases
For Decorators
Use composition to add behavior dynamically
Follow Single Responsibility Principle for each decorator
Ensure decorators can be stacked/combined
Keep decorators focused on specific behavior enhancement
Test decorator behaviors independently
Making the Right Choice
When deciding which pattern to use, ask yourself:
Is this a simple formatting or HTML generation task?
- Use a helper
Is this a reusable piece of HTML?
- Use a partial
Do I need to combine data from multiple models?
- Use a presenter
Do I need to dynamically add behavior to objects?
- Use a decorator
Do I need both logic and template encapsulation?
- Consider a View Component
Real-World Example: Complex Product Listing
Let's tie everything together with a real-world example that uses all patterns effectively:
# app/helpers/product_helper.rb
module ProductHelper
def format_stock_level(count)
case count
when 0 then content_tag(:span, 'Out of Stock', class: 'text-danger')
when 1..5 then content_tag(:span, 'Low Stock', class: 'text-warning')
else content_tag(:span, 'In Stock', class: 'text-success')
end
end
def price_display(amount)
return 'Contact for Price' if amount.nil?
number_to_currency(amount)
end
end
# app/presenters/product_presenter.rb
class ProductPresenter
include ActionView::Helpers::NumberHelper
include ActionView::Helpers::TagHelper
include ProductHelper
def initialize(product, view_context)
@product = product
@view_context = view_context
end
def display_price
price_display(@product.price)
end
def availability_status
format_stock_level(@product.stock_count)
end
def thumbnail
h.image_tag(
@product.image_url,
class: 'product-thumb',
alt: @product.name
)
end
private
def h
@view_context
end
end
# app/presenters/product_listing_presenter.rb
class ProductListingPresenter
def initialize(category, current_user)
@category = category
@current_user = current_user
end
def featured_products
@category.products
.featured
.includes(:variants)
.limit(5)
end
def recommended_products
ProductRecommendationService.new(@current_user)
.recommend_for_category(@category)
end
def category_stats
{
total_products: @category.products.count,
avg_price: calculate_average_price,
top_brands: top_brands_with_counts
}
end
private
def calculate_average_price
@category.products.average(:price)
end
def top_brands_with_counts
@category.products
.group(:brand)
.order('count_all DESC')
.limit(5)
.count
end
end
# app/decorators/discounted_product_decorator.rb
require 'delegate'
class DiscountedProductDecorator < SimpleDelegator
def price
base_price = super
return base_price if base_price.nil?
apply_seasonal_discount(
apply_volume_discount(base_price)
)
end
private
def apply_volume_discount(price)
return price unless bulk_purchase?
price * 0.9 # 10% volume discount
end
def apply_seasonal_discount(price)
return price unless seasonal_sale?
price * 0.85 # 15% seasonal discount
end
def bulk_purchase?
quantity > 10
end
def seasonal_sale?
Time.current.month == 12 # December sale
end
end
# app/decorators/premium_product_decorator.rb
require 'delegate'
class PremiumProductDecorator < SimpleDelegator
def available?
super && in_stock? && !restricted_region?
end
def warranty_period
super * warranty_multiplier
end
private
def in_stock?
quantity > minimum_stock_threshold
end
def restricted_region?
restricted_regions.include?(shipping_region)
end
def warranty_multiplier
premium_tier? ? 2 : 1
end
def minimum_stock_threshold
premium_tier? ? 5 : 1
end
def premium_tier?
price > 1000
end
def restricted_regions
%w[restricted_region_1 restricted_region_2]
end
end
# app/decorators/international_product_decorator.rb
require 'delegate'
class InternationalProductDecorator < SimpleDelegator
def price
base_price = super
return base_price if base_price.nil?
apply_currency_conversion(
apply_international_fees(base_price)
)
end
def shipping_options
return domestic_shipping if local_region?
international_shipping
end
private
def apply_currency_conversion(price)
price * currency_conversion_rate
end
def apply_international_fees(price)
price * (1 + international_fee_percentage)
end
def currency_conversion_rate
# This would typically come from an external service
ExchangeRateService.rate_for(target_currency)
end
def international_fee_percentage
0.05 # 5% international processing fee
end
def local_region?
shipping_region == 'domestic'
end
def domestic_shipping
['Standard', 'Express']
end
def international_shipping
['International Standard', 'International Priority']
end
end
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def show
product = Product.find(params[:id])
@product_presenter = ProductPresenter.new(product, view_context)
# Example of stacking decorators
@product = if international_customer?
InternationalProductDecorator.new(
DiscountedProductDecorator.new(
PremiumProductDecorator.new(product)
)
)
else
DiscountedProductDecorator.new(
PremiumProductDecorator.new(product)
)
end
end
def index
@listing = ProductListingPresenter.new(
Category.find(params[:category_id]),
current_user
)
end
private
def international_customer?
current_user.region != 'domestic'
end
end
# app/views/products/show.html.erb
<div class="product-details">
<%= @product_presenter.thumbnail %>
<h1><%= @product.name %></h1>
<div class="pricing">
<%= @product_presenter.display_price %>
</div>
<div class="availability">
<%= @product_presenter.availability_status %>
</div>
<% if @product.available? %>
<div class="shipping-options">
<%= render 'shipping_options', options: @product.shipping_options %>
</div>
<div class="warranty">
Warranty: <%= pluralize(@product.warranty_period, 'month') %>
</div>
<% end %>
</div>
# app/views/products/index.html.erb
<div class="category-products">
<div class="stats">
<%= render 'category_stats', stats: @listing.category_stats %>
</div>
<section class="featured">
<h2>Featured Products</h2>
<%= render partial: 'product',
collection: @listing.featured_products,
as: :product %>
</section>
<section class="recommended">
<h2>Recommended for You</h2>
<%= render partial: 'product',
collection: @listing.recommended_products,
as: :product %>
</section>
</div>
This comprehensive example shows how different patterns work together:
Proper Helpers:
Simple formatting utilities
View-specific helper methods
Reusable across different views
Proper Presenters:
Handle view-specific formatting
Work with view context
Format data for display
Proper Decorators:
Add behavior through composition
Can be stacked/combined
Context-independent
Modify core behavior
No view-specific logic
Complete implementation:
All necessary files included
No manual code insertion needed
Ready to use
Shows how patterns work together
Conclusion
Effective view organization in Rails requires a thoughtful combination of different patterns.
While helpers and partials provide basic organization, presenters and decorators offer more sophisticated solutions for complex scenarios.
The key is understanding when to use each pattern and how they complement each other.
Remember:
Start simple with helpers and partials
Introduce presenters when view logic becomes complex
Use decorators to keep models clean
Consider View Components for complete encapsulation
Test thoroughly, especially complex logic
Document your decisions and requirements
By following these patterns and guidelines, you can maintain clean, maintainable views even as your Rails application grows in complexity.
While there's no one-size-fits-all solution, understanding these patterns gives you the tools to make informed decisions about view organization in your applications.
Subscribe to my newsletter
Read articles from BestWeb Ventures directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
BestWeb Ventures
BestWeb Ventures
From cutting-edge web and app development, using Ruby on Rails, since 2005 to data-driven digital marketing strategies, we build, operate, and scale your MVP (Minimum Viable Product) for sustainable growth in today's competitive landscape.