Photo by Jalen O’Neal from Pexels

Which came first, the `yield` or the `content_for`?

Brandon Conway
3 min readJun 22, 2019

--

A little background

In a Rails app I have been working on for a while, there is something like this in the layout:

<!DOCTYPE html>
<html lang="en">
<head>
<%= yield :react_styles if content_for?(:react_styles) %>
</head>
<body>
<%= render 'layouts/header' %>
<%= yield %>
<%= render 'layouts/footer' %>
</body>
</html>

Sometimes I notice that it seems like there should be some content being yielded in <head>, but it isn’t. I finally confirmed it was happening yesterday, and dug into why.

The problem

After hours of debugging and seemingly going into circles, I figured out what was happening. Some pages had yielded content, while others did not. This was maddening since all of the #content_for calls happened in the same partial.

How could it be that the same partial sometimes had the expected effects and sometimes didn’t? I finally figured out that the times it worked were all in view templates which were being yielded by the application layout, while all of the situations where it was not working were coming from partials rendered directly from the application layout.

Another way to say it is, any place in layouts/header or layouts/footer that I used the partial, the content was not available for yielding in my <head>, while it was available coming any place I had used the partial in any templates that were rendered during the yield.

The cause

After making this connection, I had a direction to go with debugging. I figured out that it had to do with the order in which things were being rendered.

When the application layout compiles <head>, it can only yield whatever is in content_for(:react_styles) at that moment. The value is not lazily yielded at the end of the template compilation, as I had always just assumed (I’m not sure why I assumed that in hindsight).

Since my render 'layouts/header' and render 'layouts/footer' calls are after my yield :react_styles call in my application layout, the layout cannot yield any content they add to content_for(:react_styles).

This taught me something interesting. View templates are compiled before their layout, and then inserted at the yield. That is why any content_for(:react_styles) calls affected the yield :react_styles if they were present in anything rendered in the yielded template.

The solution

After I found and verified the cause (always verify what you think the cause is, so you don’t waste hours debugging the wrong thing!), I set off on a path to figure out how to lazily yield :react_styles.

If this is possible I didn’t find a way to do it. After a good night’s sleep (the best way to debug a problem), I did wake up with a solution that works perfectly though.

I refactored my application layout into two files that look like this:

# layouts/application.html.erb<% content_for(:body_content) { render partial: 'layouts/body_content' } %><!DOCTYPE html>
<html lang="en">
<head>
<%= yield :react_styles if content_for?(:react_styles) %>
</head>
<body>
<%= yield :body_content %>
</body>
</html>

and

# layouts/_body_content.html.erb<%= render 'layouts/header' %>
<%= yield %>
<%= render 'layouts/footer' %>

That’s it! By explicitly rendering all of the body content (including the view template) where I want to, I am essentially turning what used to be a problem causing a bug into a tool in my toolbelt.

Since all of my content_for(:react_styles) calls are happening within my render 'layouts/body_content', I know for sure that all of the content I expect to be available to yield :react_styles will be there.

If you implement this solution, make sure you add a comment in your application layout explaining why you did it, or someone (including future you) might reverse your changes because they don’t understand them.

--

--