Skip to content

Linter Rule: Prefer collection rendering over rendering a partial in a loop

Rule: actionview-prefer-collection-render

Description

Prefer render partial: "...", collection: ... over calling render for a single partial inside an each loop.

The reported message contains the exact replacement tag, built from the loop's receiver and the partial being rendered, so it can be pasted over the loop:

erb
<% @users.each do |user| %>
Prefer `<%= render partial: "user", collection: @users %>` over rendering a partial once per iteration. Collection rendering builds the partial once instead of for every item. (actionview-prefer-collection-render)
<%= render "user", user: user %>
The partial `user` is looked up relative to the directory of the template rendering it. Moving this template changes which file that resolves to, and renaming the partial means hunting for callers that never spell its full name. Write the full path from the view root so it resolves to the same file from any template, and a search for that path finds every caller. (actionview-prefer-qualified-partial-path)
<% end %>

Collection rendering names the local after the partial, so when the loop passes the element under a different name the replacement carries an as: to keep the partial working:

erb
<% @gems.each do |topic_gem| %>
Prefer `<%= render partial: "gem_card", collection: @gems, as: :topic_gem %>` over rendering a partial once per iteration. Collection rendering builds the partial once instead of for every item. (actionview-prefer-collection-render)
<%= render partial: "gem_card", locals: { topic_gem: topic_gem } %>
The partial `gem_card` is looked up relative to the directory of the template rendering it. Moving this template changes which file that resolves to, and renaming the partial means hunting for callers that never spell its full name. Write the full path from the view root so it resolves to the same file from any template, and a search for that path finds every caller. (actionview-prefer-qualified-partial-path)
<% end %>

Rendering an object directly reports the shorthand collection form instead:

erb
<% @users.each do |user| %>
Prefer `<%= render @users %>` over rendering a partial once per iteration. Collection rendering builds the partial once instead of for every item. (actionview-prefer-collection-render)
<%= render user %>
Rails derives the partial from `to_partial_path` on `user` when the template renders, so the template this renders is not named in this `<%= render %>` call. Name it explicitly, with `object:` for a single record or `collection:` for many, and Herb can take you to it, check the locals you pass against its strict locals, and help you rename them. (actionview-no-implicit-partial)
<% end %>

Rationale

When a partial is rendered inside a loop, Action View looks the template up and sets up a fresh local scope on every iteration. Collection rendering does that work once and then reuses it for every element, so it is meaningfully faster for anything but the shortest collections.

Collection rendering also passes each element as a local named after the partial, and provides a <partial>_counter local, which removes the need to thread the loop variable through by hand. When the loop passes the element under a name that isn't the partial name, the suggestion adds as: so the partial keeps receiving the local it expects.

Because the rewrite emits the partial and nothing else, this rule only fires when the loop body is exactly one output render and the only local passed is the block argument. Loops that wrap the partial in markup, pass extra locals, or use a block argument the partial doesn't receive are left alone, since collection rendering cannot express them.

Examples

✅ Good

erb
<%= render partial: "user", collection: @users %>
The partial `user` is looked up relative to the directory of the template rendering it. Moving this template changes which file that resolves to, and renaming the partial means hunting for callers that never spell its full name. Write the full path from the view root so it resolves to the same file from any template, and a search for that path finds every caller. (actionview-prefer-qualified-partial-path)
erb
<%= render partial: "gem_card", collection: @gems, as: :topic_gem %>
The partial `gem_card` is looked up relative to the directory of the template rendering it. Moving this template changes which file that resolves to, and renaming the partial means hunting for callers that never spell its full name. Write the full path from the view root so it resolves to the same file from any template, and a search for that path finds every caller. (actionview-prefer-qualified-partial-path)
erb
<%= render @users %>
Rails derives the partial from `to_partial_path` on `@users` when the template renders, so the template this renders is not named in this `<%= render %>` call. Name it explicitly, with `object:` for a single record or `collection:` for many, and Herb can take you to it, check the locals you pass against its strict locals, and help you rename them. (actionview-no-implicit-partial)

Loops that do more than render a single partial are not flagged, because collection rendering cannot express them:

erb
<% @users.each do |user| %>
  <li><%= render "user", user: user %></li>
The partial `user` is looked up relative to the directory of the template rendering it. Moving this template changes which file that resolves to, and renaming the partial means hunting for callers that never spell its full name. Write the full path from the view root so it resolves to the same file from any template, and a search for that path finds every caller. (actionview-prefer-qualified-partial-path)
<% end %>
erb
<% @users.each do |user| %>
  <%= render "user", user: user, admin: true %>
The partial `user` is looked up relative to the directory of the template rendering it. Moving this template changes which file that resolves to, and renaming the partial means hunting for callers that never spell its full name. Write the full path from the view root so it resolves to the same file from any template, and a search for that path finds every caller. (actionview-prefer-qualified-partial-path)
<% end %>
erb
<% @users.each_with_index do |user, index| %>
Block argument `index` is never used. Use `each` instead of `each_with_index`, or prefix it with an underscore as `_index` to show it is intentionally unused. (erb-no-unused-block-argument)
<%= render "user", user: user %>
The partial `user` is looked up relative to the directory of the template rendering it. Moving this template changes which file that resolves to, and renaming the partial means hunting for callers that never spell its full name. Write the full path from the view root so it resolves to the same file from any template, and a search for that path finds every caller. (actionview-prefer-qualified-partial-path)
<% end %>

🚫 Bad

erb
<% @users.each do |user| %>
Prefer `<%= render partial: "user", collection: @users %>` over rendering a partial once per iteration. Collection rendering builds the partial once instead of for every item. (actionview-prefer-collection-render)
<%= render "user", user: user %>
The partial `user` is looked up relative to the directory of the template rendering it. Moving this template changes which file that resolves to, and renaming the partial means hunting for callers that never spell its full name. Write the full path from the view root so it resolves to the same file from any template, and a search for that path finds every caller. (actionview-prefer-qualified-partial-path)
<% end %>
erb
<% @users.each do |user| %>
Prefer `<%= render partial: "user", collection: @users %>` over rendering a partial once per iteration. Collection rendering builds the partial once instead of for every item. (actionview-prefer-collection-render)
<%= render partial: "user", locals: { user: user } %>
The partial `user` is looked up relative to the directory of the template rendering it. Moving this template changes which file that resolves to, and renaming the partial means hunting for callers that never spell its full name. Write the full path from the view root so it resolves to the same file from any template, and a search for that path finds every caller. (actionview-prefer-qualified-partial-path)
<% end %>
erb
<% @users.each do |user| %>
Prefer `<%= render @users %>` over rendering a partial once per iteration. Collection rendering builds the partial once instead of for every item. (actionview-prefer-collection-render)
<%= render user %>
Rails derives the partial from `to_partial_path` on `user` when the template renders, so the template this renders is not named in this `<%= render %>` call. Name it explicitly, with `object:` for a single record or `collection:` for many, and Herb can take you to it, check the locals you pass against its strict locals, and help you rename them. (actionview-no-implicit-partial)
<% end %>
erb
<% @gems.each do |topic_gem| %>
Prefer `<%= render partial: "gem_card", collection: @gems, as: :topic_gem %>` over rendering a partial once per iteration. Collection rendering builds the partial once instead of for every item. (actionview-prefer-collection-render)
<%= render partial: "gem_card", locals: { topic_gem: topic_gem } %>
The partial `gem_card` is looked up relative to the directory of the template rendering it. Moving this template changes which file that resolves to, and renaming the partial means hunting for callers that never spell its full name. Write the full path from the view root so it resolves to the same file from any template, and a search for that path finds every caller. (actionview-prefer-qualified-partial-path)
<% end %>

References

Released under the MIT License.