Which came first, the `yield` or the `content_for`?
--
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 yield
ed by the application layout, while all of the situations where it was not working were coming from partials render
ed 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 yield
ing 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 yield
ed 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 yield
ed 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.