Rails Views Patterns
Follow standard Rails view conventions for ERB templates, partials, layouts, helpers, form_with, and content_for regions. This skill covers only the opinionated and non-obvious additions.
Applies on the html delivery axis (Axis B, rails-stack-profiles). On the api axis there are no app views — use rails-api-patterns instead.
See patterns.md for detailed examples.
Quick Reference
| Area | Key Rule | Anti-Pattern |
|------|----------|--------------|
| Partials | Extract reusable components | Duplicated HTML across views |
| Collections | Always use render collection: | Manual loops with each |
| Caching | Fragment cache expensive renders | No caching on collection renders |
| Accessibility | Semantic HTML + ARIA | <div> soup without roles |
| Logic | Presenter or helper for complex logic | Conditionals in templates |
Core Principles
- Views are for presentation only -- no queries, no mutations, no business logic
- Semantic HTML5 -- use
<header>,<nav>,<main>,<article>,<section>,<aside>instead of generic<div>wrappers - Always
render collection:-- nevereach+renderinside the loop (performance + enables collection caching) - Cache aggressively -- fragment caching with
cached: trueon collections, Russian-doll nesting for parent/child
Presenter Objects and ViewComponents
Where complex view logic goes is governed by an orthogonal project fact, not an architecture axis: whether the view_component gem is installed. Read it from the project-conventions fingerprint (Frontend category).
view_componentgem present: use ViewComponents — see patterns.md.- No
view_componentgem: extract complex view logic into plain Ruby presenter objects.
# app/presenters/dashboard_presenter.rb
class DashboardPresenter
attr_reader :user, :period
def initialize(user:, period: Date.current.all_month)
@user = user
@period = period
end
def grouped_activities
user.activities.where(date: period).group_by(&:category).sort_by { |cat, _| cat.position }
end
def empty? = user.activities.where(date: period).none?
def title = "#{user.name}'s Dashboard"
end
<%# Clean view -- no logic %>
<h1><%= @presenter.title %></h1>
<% @presenter.grouped_activities.each do |category, activities| %>
<%= render partial: "activity", collection: activities, cached: true %>
<% end %>
Anti-Patterns
| Bad | Good | Why |
|-----|------|-----|
| <% @users = User.all %> in view | Pass @users from controller | Views must not query |
| <% if user.admin? && user.active? && ... %> | Extract to helper or presenter | Logic belongs elsewhere |
| <% @products.each do \|p\| %> + render | render partial:, collection: | Performance + caching |
| <div onclick="..."> | Stimulus controller | Unobtrusive JS |
Accessibility Checklist
- [ ] All images have meaningful
altattributes (emptyalt=""for decorative images) - [ ] Form fields have associated
<label>elements (not just placeholder text) - [ ] Interactive elements are keyboard-accessible
- [ ] Color contrast meets WCAG AA (4.5:1 for text, 3:1 for large text)
- [ ] ARIA landmarks on major page regions (
role="navigation",role="main", etc.) - [ ] Skip-to-content link as first focusable element in
<body> - [ ] Error messages linked via
aria-describedbyand announced withrole="alert" - [ ] Flash messages wrapped in
aria-live="polite"region
Output Format
When reporting on view quality, use:
## View Analysis: [file_path]
**Issues Found:**
- [severity] description -- suggested fix
**Recommendations:**
1. actionable recommendation
2. ...