Benefit of app layout being defined inline in Layouts, rather than in heex file?

I understand that heex code can either be defined inline with the ~H sigil, or placed into a .heex file and embedded with embed_templates.

I’m curious why Phoenix puts the root layout in a heex file, but the defines the app layout inline.

Is there a specific benefit to defining the app layout inline? Or is it simply because the default app layout is pretty small? Or maybe to provide an example of each method?

Layouts are not function components and vise versa. The root layout is still a „layout“ handled by phoenix layout tooling, while latest phoenix no longer uses said layout tooling for the app layout. It wraps page templates in the <Layout.app /> function component.

Is this true? As far as I can see, both Layouts.root and Layouts.app are function components. root is included in Layouts via embed_templates, whereas app is defined inline.

On that end yes, but the code calling layouts doesn’t really use it as a function component. There’s no slots, no handling of attributes, the page content is passed as a magic {@inner_content}. So yes root.html could technically be inlined, but it would likely make people question those other limitations rather quickly.

PS. Also embed_templates is not at all scoped to just heex and function components.

1 Like

I see, so root is technically a function component, but it is treated specially by Phoenix.

So back to the question: what’s the benefit of inlining app rather than putting it in a heex file?

Personally I find attr/slot documentation in combination with external template files quite hard to follow. I cannot speak of the reasons of the person who did the change though.

1 Like

Phoenix started as a web framework like Rails or Django. These frameworks use templates to render HTML with injected variables. The templating engines (e.g. Jinja) use an inheritance pattern for extensibility, where a parent template defines overridable “blocks” that children can render themselves within.

Modern “app” frameworks like React moved to a component model inspired by functional programming. Instead of inheritance the components are composed. Composition is superior to inheritance; even OOP advocates have come to accept this.

Layouts are a property of the app (not the framework) and can be implemented with inheritance or composition. Inheritance is obvious: the parent template is the layout. Composition is also fairly straightforward:

def my_page(assigns) do
  ~H"""
  <.layout>
    <div>What a cool page!</div>
  </.layout>
  """
end

def layout(assigns) do
  ~H"""
  <div>
    <.nav />
    <main>{render_slot(@inner_block)}</main>
  </div>
  """
end

Phoenix is fundamentally built on templates, but I assume the OG Phoenix team saw that composition was superior (Elixir is a functional language!), which is why the inheritance implementation is deliberately degenerate (only one level).

But then LiveView came along and lots of holes had to be drilled into these abstractions because underneath Phoenix is fundamentally a web framework and not an app framework. And those holes, well, leak.

The root layout exists explicitly to be the part of the page that is rendered outside of the LiveView mount. That is why it’s treated separately from the other “layouts”, which are now components (as they should be). If you want to see where the abstraction is really leaking, look no further than live_title/1. That one hurts a bit.

Anyway, the answer to your question was already implied above: the root layout is a layout template (inheritance) while Layouts.app is a layout component (composition).

I assume Layouts.app was inlined in a module to encourage users to define more layouts (similar to CoreComponents). On the other hand, you do not want to define many root layouts because anything inside the root layout cannot be touched by a LiveView app. So it should be minimal.

Explicitly pointing out that templates use inheritance and components use composition helps a lot, thanks.

I already replied to @LostKobrakai about this above. Contrary to what you both say, the root layout is actually a function component. I can call it with Layouts.root and see it, just like I can Layouts.app. So I suppose it’s used as a “template” with “inheritance” by Phoenix only conceptually, by giving it special treatment.

1 Like

`root` could work as a regular function. I just double checked by converting it in tidewave.ai and it worked just fine! And I believe this is true since Phoenix v1.6 (or v1.7?) when we unified everything to be function components.

So let’s answer this by parts.

First, why do we need layouts?

Mainly because LiveView pages need to denote some content that cannot be live updated. This is typically in the `head` tag. We know there are frameworks who have attempted to update and merge `head` tags but it is very easy to run into hard-to-debug JavaScript bugs and corner-cases in browser. So the root layout helps us denote the static part.

Who renders the layout? After all, we could have made it so all of your regular templates simply called `<Layout.root>`.

Per the above, we know that the contents of `<Layout.root>` cannot be updated and therefore they should not be re-rendered. If we made your template call it, the one rendered on every LiveVIew update, then we would always re-render the root layout too, only to later figure out a way to discard its updates, which would be wasteful.

Beyond the above, it is important to note that frameworks generally want to own how the layout is rendered because it can lead to UX improvements such as sending the layout upfront, up to the `body` tag, before your LiveView and Controllers get invoked. This would allow browsers to parse `head` tag while you are still runing queries and what not, leading to faster page loads. Of course, there are complications, such as what to do if you send the `head` and then you get an exception, but the point is that leaving the framework to control how the layout is rendered can be feature!

IMO, this is not about inheritance at all, especially because frameworks that do offer template inheritance allow you to extend the layout with additional information beyond the inner block, which we don’t. We intentionally keep the layout decoupled from the application contents because we want to have flexibility over it.

So why do we have root.html.heex?

The reason we moved the app layout to a component is because you can declare your own attributes and slots and we wanted to make it clear you should invoke it. You can’t do any of that with root layouts, so we kept them distinct to avoid confusion. Leading someone to think they should call `Layout.root` would most likely lead to disaster. :smiley:

TL;DR: the `app` and `root` layouts are defined differently as to signal users you are supposed to invoke the former but not the latter. That’s it.

7 Likes

For those who want to: phoenix_live_head | Hex

The change to a layout component was a major improvement for all the reasons José mentions. It also removed one extra segment of the docs now it’s clear in every template where the layout comes from.

2 Likes

It’s also worth pointing out that this pattern is essentially universal. Even full SPAs are generally mounted under the body tag. This indicates to me that the <head> really does not belong within the app’s control. TBH there is probably an argument to be made that <head> is an unreasonable overloading of HTML itself.

So clearly there is a need for something shaped like the “root layout” somewhere. Not really as a layout, but as a “mounting point” or “<head> container”.

I really like the new Layouts.whatever components, conceptually.

I think the only actual problem here is the overloading of the term “layout”. Root layout and app layout components are clearly totally different things. Maybe the root layout should be renamed to something else? “Root template”? Just “root”?

I only meant to present this inheritance/composition split as an explanation for how these things evolved. Like I said, the root layout uses a pattern reminiscent of inheritance in pre-component frameworks like Django/Rails, but the root layout was always intentionally degenerate in Phoenix. There is only one level of inheritance.

And I am only using the term “inheritance” because I think it’s an interesting way of looking at it. If I say “this is a degenerate rectangle” and you say “that’s a line segment” we are both correct :slight_smile:

I don’t think that’s quite accurate. While it’s true that frontend frameworks typically mount inside the body tag, they still need to manage the <head> dynamically. That’s why all modern frameworks either have built-in features or standard solutions for dynamically updating meta tags, page titles, OpenGraph tags, scripts, stylesheets, and other head content based on the current page and application state.

The need for a static mounting point is true for LiveView specifically, but for different reasons than you’re implying. As José explained, it’s about LiveView’s architecture and how it handles updates.

Hologram takes a simpler approach: the full layout including the <head> is just a regular component. You can pass props to it from your pages for the various head elements mentioned above, and it can have its own state or emit context data. You can even send events (actions/commands) to it from any component in the tree. This greatly simplifies the mental model and improves DX since there’s no special “root layout” vs “app layout” distinction - it’s all just components composing together.

1 Like

Then why don’t they mount above the head? Do you think this is a historical accident?

My assumption is that reusing the same abstraction for <body> and <head> has proved problematic which is why it’s not widely done. But if you have found this to be a good pattern then perhaps I am simply wrong.

I don’t see any reason why it would be more or less true for LiveView or React or any other framework. If this pattern is actually fine as you suggest it should be possible to make LiveView work this way.

Meta tags and OG tags are generally things that are rendered statically, right? Like, it doesn’t make semantic sense to update OG tags based on client-side JS because their entire purpose is to be consumed as HTML by a scraper. CSS and JS are also generally static in this way.

Page titles are the odd one out. In React you would call useEffect as glue to transform the imperative document.title into a declarative API. In LiveView there is the live_title thing, which is a less general solution but fundamentally the same idea.

Can anyone think of any more examples of <head> children which you would update dynamically?

My suspicion is there’s significant historical inertia at play. Early SPAs were purely client-side - your server sent a minimal HTML shell with a static <head>, and JavaScript mounted in the body. This wasn’t really a choice. Without SSR, you needed that pre-rendered for bootstrap. The ecosystem then built around this pattern (React Helmet, etc.), and by the time SSR became standard, there wasn’t enough reason to revisit the architecture when the workarounds were “good enough”.

2 Likes

There are legitimate user-facing dynamic head updates: favicons (notification badges), page-specific scripts, A/B testing scripts, page-specific widgets (chat, analytics), theme metadata (theme-color), and viewport metadata.

You could create separate layouts for pages that need different head content, but if the only difference between pages is in the <head>, it’s much cleaner to just pass those as props to a single layout component. No need to duplicate the entire layout structure.

But here’s the broader point: even if the metadata is static and only matters for initial server-side rendering, treating the layout with its head section as a single component is still clearer. No special cases, no mental overhead of “this part uses the framework’s component model, but this other part uses a separate template system” or some impeative glue. It’s just components all the way down, which significantly simplifies the mental model.

2 Likes

The favicon is a good one, and there is a glaring absence of a live_favicon to complement live_title. I see your fellow Bart @BartOtten also got around to that one!

Page specific scripts, a/b testing, widgets, and themes are all things that I would want to handle within the framework rather than linking JS in the head. I guess you could say “in the real world people do this”, but it seems wrong.

Viewport is an interesting one. How often do you change this in an SPA? I guess it could happen.

I would point out that it’s imperative glue all the way down. The DOM APIs are imperative too.

I am sympathetic to your argument here, and I’d have to think about it more before committing either way. I think the best counter would be to say that it seems like many of the things in <head> don’t really feel like “elements” or have weird side-effects in a way that elements in <body> do not. I think that’s also what Jose was getting at.

I’ll think about it more. Thanks for the examples.

If that’s the case, it is interesting that Phoenix managed to inherit the same pattern despite starting with server rendering, isn’t it?

2 Likes