Rails View Patterns: Helpers vs Partials vs Presenters vs Decorators

BestWeb VenturesBestWeb Ventures
10 min read

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

  1. Format data consistently across views

  2. Generate common HTML structures

  3. Create reusable UI components

  4. Handle simple conditional logic

  5. 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

  1. Extract repeated view fragments

  2. Break down complex views into manageable chunks

  3. Share common UI elements across different views

  4. Create reusable form templates

  5. 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

  1. Complex calculations involving multiple models

  2. View-specific data transformations

  3. Complex conditional logic

  4. Dashboard-style data aggregation

  5. 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

  1. Add computational methods to models

  2. Transform or enhance data

  3. Add business logic that doesn't belong in the model

  4. Implement cross-cutting concerns

  5. 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:

  1. Is this a simple formatting or HTML generation task?

    • Use a helper
  2. Is this a reusable piece of HTML?

    • Use a partial
  3. Do I need to combine data from multiple models?

    • Use a presenter
  4. Do I need to dynamically add behavior to objects?

    • Use a decorator
  5. 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.

1
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.