Skip to main content
  1. Posts/

Extending Rails Partials with local_assigns (and Avoiding the Nil-Guard Trap)

·5 mins
Justin Smestad
Author
Justin Smestad
Table of Contents

Shared partials are where Rails views go to rot.

Every partial starts clean: extract some duplicated HTML, render it from a few places, move on. Then someone adds a local variable. Then a conditional. Then every caller needs to pass blog_uri: nil just to avoid a NameError, and suddenly your “simple” partial is a maintenance burden that confuses every new developer who reads it.

Rails has had a built-in solution for this since before most of us started using the framework. It’s called local_assigns, and it’s so old that even DHH forgot it existed. This post walks through how partials typically degrade and how local_assigns keeps them clean.

A quick note: the examples here use partials with local variables, not instance variables. Local variables are the better default for partials because they make the data dependency explicit at the call site. That’s a separate argument, but it’s why you’ll see render 'footer', blog_uri: ... instead of @blog_uri throughout.

Stage 1: The clean extraction
#

You have a footer that shows up on every page. Privacy policy, terms of service, copyright. You extract it into a partial:

<%= render 'footer' %>
<footer>
  <ul class="inline">
    <li><%= link_to 'Privacy Policy', privacy_policy_url %></li>
    <li><%= link_to 'Terms of Service', terms_of_service_url %></li>
  </ul>
  <p>&copy; 2017 Overstuffed Gorilla LLC</p>
</footer>

No logic, no variables, no problems. This is the honeymoon phase.

Stage 2: Add some dynamic data
#

A feature request comes in: show the user’s blog and Facebook links above the company footer on logged-in pages. You add two local variables:

<%= render 'footer', blog_uri: @current_user.blog_uri, facebook_uri: @current_user.facebook_uri %>
<footer>
  <section class="user-links">
    <ul class="inline">
      <li><%= link_to 'Blog', blog_uri %></li>
      <li><%= link_to 'Facebook', facebook_uri %></li>
    </ul>
  </section>
  <section class="company-links">
    <ul class="inline">
      <li><%= link_to 'Privacy Policy', privacy_policy_url %></li>
      <li><%= link_to 'Terms of Service', terms_of_service_url %></li>
    </ul>
  </section>
  <p>&copy; 2017 Overstuffed Gorilla LLC</p>
</footer>

This works on logged-in pages. But you’ve made a big assumption: every page that renders this partial now needs to provide those two variables.

Stage 3: It breaks
#

The login page, the 404 page, the marketing site; none of them have a current_user. They all render the footer. They all blow up with NameError: undefined local variable or method 'blog_uri'.

Your first instinct is to add a conditional inside the partial:

<% if blog_uri && facebook_uri %>
  <section class="user-links">
    ...
  </section>
<% end %>

This looks right but doesn’t work. The if statement still evaluates blog_uri, which triggers the same NameError when the variable was never passed. Ruby doesn’t know the variable exists at all; it’s not nil, it’s undefined.

The anti-pattern: nil guards everywhere
#

The most common fix I’ve seen in the wild is to pass explicit nils from every caller that doesn’t have the data:

<%= render 'footer', blog_uri: nil, facebook_uri: nil %>

Or worse, with inline conditionals:

<%= render 'footer',
    blog_uri: (@current_user ? @current_user.blog_uri : nil),
    facebook_uri: (@current_user ? @current_user.facebook_uri : nil) %>

This eliminates the error, but it creates a different problem. Every caller now has to mention variables that have no meaning in its context. A developer reading the login page template sees blog_uri: nil and reasonably asks: “Why would there ever be a blog URI on the login page? Is this dead code? Should I remove it?”

That confusion multiplies with every optional variable you add. Three optional variables means every caller needs three nil assignments. Five variables, five assignments. The partial was supposed to reduce duplication, and now it’s creating a different kind of it.

The fix: local_assigns
#

local_assigns is a hash that Rails makes available inside every partial. It contains only the variables that were actually passed to that render call. If a variable wasn’t passed, it’s simply not in the hash. No NameError, no nil guards.

<%# Logged-in pages pass the user links %>
<%= render 'footer', blog_uri: @current_user.blog_uri, facebook_uri: @current_user.facebook_uri %>

<%# Everything else just renders the footer %>
<%= render 'footer' %>
<footer>
  <% if local_assigns[:blog_uri] && local_assigns[:facebook_uri] %>
    <section class="user-links">
      <ul class="inline">
        <li><%= link_to 'Blog', local_assigns[:blog_uri] %></li>
        <li><%= link_to 'Facebook', local_assigns[:facebook_uri] %></li>
      </ul>
    </section>
  <% end %>
  <section class="company-links">
    <ul class="inline">
      <li><%= link_to 'Privacy Policy', privacy_policy_url %></li>
      <li><%= link_to 'Terms of Service', terms_of_service_url %></li>
    </ul>
  </section>
  <p>&copy; 2017 Overstuffed Gorilla LLC</p>
</footer>

The partial handles both cases cleanly. Callers that have the data pass it. Callers that don’t just skip it. No nil guards, no confusion, no extra variables cluttering templates where they don’t belong.

Why this matters beyond the syntax
#

Partial complexity is a leading indicator of view-layer health. When you see partials accumulating boolean flags, nil guards, and mode switches, it usually means the abstraction boundary is in the wrong place or the interface wasn’t designed to handle optional behavior.

local_assigns is a small tool, but it solves the right problem: it lets a partial have a flexible interface without pushing that flexibility cost onto every caller. The same principle applies everywhere in software design. The component that offers optional behavior should handle the optionality internally, not force every consumer to manage it.

In my experience, view code is the most likely place for this kind of accidental complexity to accumulate unchecked. Backend code gets reviewed for design. Views get reviewed for “does it look right.” Patterns like local_assigns are how you keep the view layer from quietly becoming the most expensive part of your codebase to change.