~H sigil everywhere vs .heex files - any real world experience?

I’ve been rethinking how I organize templates in Phoenix apps. The standard approach (at least the scaffold and what I consider standard) is separate .html.heex files, but I’m leaning toward using ~H sigil for everything instead.

Why?

The mixed approach bothers me. You end up with implicit rules that nobody enforces: “small components inline (like core_components.ex), big ones (like a live view or a controller or even a toolbar) in files” - but what’s the threshold? “Reusable stuff gets files” - how reusable? These fuzzy boundaries makes me wonder every time I add something.

With all-~H, you get a predictable module structure. Logic and handlers at the top, all rendering at the bottom (at least that’s how I see it). You can scroll to the end of any LiveView and immediately see what it renders. No jumping between files.

More importantly, you can define function components directly inside your template. Need a small helper? Just write a function right there above the main ~H block. This keeps template-specific helpers colocated without polluting your module’s public interface or creating separate files for tiny pieces.

Cons:

Tooling. LSP features, syntax highlighting, formatters all work better with dedicated .heex files. That’s real friction (or is it?)

The other common argument is “designers need separate files.” But honestly, how many teams actually have non-Elixir people editing templates? And even with .heex files, you still need to understand assigns, components, and Phoenix conventions.

Any story that could help?

Has anyone actually done this at scale? I’m talking “old” (maintained, iterated), multiple devs, real production app. Did the tooling gap become painful? Did you regret it?

Or did anyone try this and switch back to separate files? What broke the camel’s back?

I’m less interested in theoretical arguments and more in “I tried this and here’s what happened.” The ecosystem pushes separate files more (now with embed_templates), but I want to hear from people who’ve actually tested the alternative.

5 Likes

I’m all kinds of unqualified to answer this but two things:

Why have a threshold, why not just always embedded? That’s what we do (and what I always do). Our app has six devs, a little over a year old, not launched yet though lots of customer testing (we have other apps). Though, ya one of us uses an IDE not meant for Elixir which does not highlight heex.

I assume you are using classic controllers? It’s definitely the opposite for LiveViews as all examples in the documentation use embedded (other than the part that shows you how to do it the other way, of course, lol). I’ve been using controllers a bunch in a personal project and use embedded because I’ve grown to prefer after using LiveView for so long.

Again, apologies for piping up without the proper qualifications to answer.

1 Like

Separate template files are really inherited baggage from Phoenix’s roots as an actual web framework rather than the app framework LiveView has grown into. Unfortunately this is not the only example of “web framework thinking” weighing LiveView’s app capabilities down.

In the JS world the React people figured out pretty quickly that templates are the enemy and must be done away with entirely. They inverted the model and built up the DOM in code rather than by gluing strings together (React.createElement('div', ...)) and then they built a DSL that made that approach pretty (JSX). Incidentally this approach was directly descended from a PHP framework called XHP. Unfortunately a lot of people do not understand this distinction and mistakenly believe that JSX is a template syntax.

HEEx was descended from EEx, which I assume came from ERB (Ruby), which in turn has a lineage back through JSP (Java), ASP, and then probably PHP. I wasn’t there, though, so who knows.

EEx is a templating language, but it does embed real Elixir code (as opposed to a degenerate sublanguage like Jinja). HEEx, by actually parsing and validating the HTML, was a good step towards “JSX nirvana” in that the templates were no longer just strings. And Elixir, being a functional language, actually lends itself more to this approach in that the template is a function and can evaluate like one.

Unfortunately HEEx has since grown a number of “DSL” capabilities (:if and :for) which are a complete 180 from this path. Frankly the more I think about it the more I am convinced this was a significant mistake.

LiveView, unlike HTMX and Hotwire and others, is actually an app framework - like React. It can be used to build stateful “real” apps which happen to be server-rendered. This is significantly different from web frameworks, which are tools used to glue a bunch of forms together.

App frameworks thrive on components rather than templates and views, which is why LiveView naturally grew them. (Unfortunately I think there may be some denial about the fact that those components need state, but I digress.)

In “app framework” world there is no distinction between “templates” and “code”, there is just code. JSX is just JS with fancy syntax. There is no template.

And so the question of separating the template is fundamentally moot. There should be no template.

This is the source of the dissonance you’re feeling.

8 Likes

Treesitter handles embedded languages just fine. I actually use ~SH for my heex (long story) and making that work was just a one-line nvim config change. I think this is a non-issue.

The formatter handles HEEx fine (it’s our own tooling after all). Not that I have ever used it or anything.

Probably zero, but even if they did what difference does it make if they edit a .ex file instead? It’s all the same stuff anyway.

(Do not believe his lies, he is fully qualified to answer.)

LOL, ya that was probably worded wrong, I just I meant I do not meet the criteria that was asked of being a multi-year project as well as I’m not really an LSP user (though I have my own monsterous solutions for the LSP-like features I actually care about).

1 Like

I can definitely be an issue when people use non-treesitter compatible editors/IDEs. Of course, they probably really shouldn’t be doing that unless they know how to fix it (I’m in that camp for with Vim and had to add HEEx indentation and highlighting myself) but some people are just really used to a particular editor/IDE and have a really hard time switching.

1 Like

For me this is simple … I use a ~H sigil for every component, so I use it together with props and slots. The .heex files are for a pages where a state may be dynamic and controlled by the LiveView. As far as I know this is a default way you have when generating files using phx generators. :light_bulb:

I think this is a good explanation of my feeling. Basically we are not working with templates but with components, even “large ones”.

I guess I should just go with my guts and use .html.heex files less as they often introduce friction in my architecture.

1 Like

My point is, .heex files, even for a page, introduces some discomfort. Live view render() are also components. I think this is the core of what I was feeling weird about.

Treating live view like any other component makes everything more consistent.

I almost never use separate heex files fwiw. It’s all the same, so do what feels best for you :slight_smile:

We try to strike a balance w/ the generators. The only exception for me is largely static or massive markup “pages” that don’t really make sense to collocate. Don’t overthink it :slight_smile:

6 Likes

The two frontend devs on my team edit templates themselves

1 Like

I would say that the only difference is when you are working in a bigger team and there is a big change in design or something like that. As a team leader you may prefer to separate page templates from LiveView logic. It’s easier to track changes made only in the logic or template if they are separated.

This has nothing to do with components and live components as they are supposed to be used on multiple pages. Therefore only pages should have templates in separate .heex files, so you can search using git for a specific type of changes simply by logging all .heex or .ex files.

Of course as said technically there is no big difference and therefore people especially in their own projects are fully free to pick their favourite solution. The bet things in Phoenix is that it’s technically a library and not a framework as there is not much stuff required by some convention and developers have lots of flexibility in how they prefer to organise their code.

However for me if there is specific reason for adding .heex files even if it’s limited to very specific cases and there is no specific cases for the opposite way then I prefer to “standardise” it and simply do so in all my projects. After all even I can’t say how big team may be working on the pet project I’m working on right now. It’s especially better since once decision is made it doesn’t take any extra time and I don’t lose further time on investigating “what I prefer” for every project.

The only exception here are scripts, but scripts have their own rules. Obviously a small script is preferred to have a single file source, so they usually don’t use .heex files. As said scripts are not projects, so they live on their own rules.


For me that’s all and I never saw .heex files as introducing any kind of “:discomfort”. I believe it’s rather a personal perspective i.e. how you are used to work with templates. This is the only thing that can introduce a “discomfort” as we simply do the usual thing in a way we are not used to. I would say that the “discomfort” feeling is often wrongly taken.

People should try various things and see on themselves what work for them. Everyone should create their own standards based on nothing else than a practice. If a feeling is not based on it then it often turns into a false assumptions which depending on person may be hard to figure out, admin and/or change in future.

1 Like

You were not replying to me, but I want to highlight this particular part because I think it’s a good example of what I disagree with here. (I agree with the rest of your post.)

The distinction between logic and template is not real. Allow me to demonstrate.

Here is logic in the template:

<div>Current count: {@first_count + @second_count}</div>

And here is template in the logic:

socket = assign(socket, :color_class, "color-red")

The correct approach, in my opinion, is to demolish this barrier and excise the concept of a template from the equation entirely. I have no idea if this could or should even be done with Phoenix or if a new framework would be needed.

I wholeheartedly agree with this and I am not a big fan of “what standard should we follow”-type discussions, which is why I went out of my way not to answer the question in my reply :slight_smile:

For the record, I also make judicious use of separate .heex templates for larger components.

“Hello world” is not good as a example. Recently people also say that “Lorem ipsum” is not the best. There is no class (see my point about huge changes in design) and the code is so simple that it could be in fact a component and not a page. This only proves my point. :wink:

For sure there are some “common” standards. I call them “community standards”. A simple example here is using Phoenix context. Such practices are proved in production by thousands of devs, so such commonly used “standards” are always worth to consider or at least start with. :light_bulb:

However my preference have changed over years and so I’m organising my files in completely different way. First of all I prefer context/context.ex naming over context.ex and context (directory) structure (especially important in large codebase). So on top of said “community standards” I define my own which is placing all files in context lib and divided by ecto schema (logic, db, templates and even tests), so for example I’m not using my_app_web at all and I have everything in lib/my_app/accounts and lib/my_app/accounts/user. :exploding_head:

I listen to others and on top of “good practices” I build my own preferences. I believe such approach is well balanced between working on too generic rules and “design everything on my own” ways. :thinking:

2 Likes

For me what really matters is experience in maintaining and working with or without templates. I am not really looking for a standard. I plan of ditching most of my .heex because I feel friction with it. It might not be standard, but I think I’ll do it. But I had some “wonderment” about it being a good idea in the long run.

1 Like

I am operating under the assumption that you, as an experienced developer, are more than capable of expanding my one-line examples into real-world code in your head.

If not then forget about it, but I feel like if you tried you would have no problem understanding my intent!

Early abstraction is always bad regardless of what you’re writing, an old developer once said: put everything in one file, until you’re forced to abstract. At that point it will be clearer and you avoid over-abstracting. This is obviously an iterative approach and you’ll rewrite your code over and over again, much like writing a book and getting your sentence and paragraphs more concise.

2 Likes

Yes, that is quite my feeling. I was always creating a separate template for my live views, and always had the feeling that it was not really separated for a good reason. I guess I kept doing it because I was influenced by the default generators.

I’m happy and reassured of our discussion, seeing I’m not alone and also, having a good explanation of the app/web framework thing made a lot of sense to me.

The point of my last reply was that it’s not a private message only for me, but only on forum. People (especially in “AI-era”) absolutely love to copy-paste stuff without thinking. Some time ago I believed that developers are a group of smart people that would not allow push themselves into any edge, but I was proved wrong as people either are scared or fascinated about “AI” - almost all I see “I fear that I would lose a job” or “how to do literally everything in AI” type of posts/responses/blogs and so on …

Emm … how to say that … Firstly thanks for your trust, but it’s not how it works … It’s your point and you have to prove it. You can’t just ask people to prove your point for you.

As said for me counter more fits component than page and all you show is assign of some class. I said that I would rather split the page on a logic and a template files for better tracking git changes. Therefore I would rather not write such code anyway. I would assign for example a boolean for a dark version of element or a theme atom and set the proper class in the template.

So no, I can’t see your point and it’s not because I’m not enough experienced. We just talk about completely different approaches. I don’t know or prefer yours, so I would not provide better example than you. Am I wrong somehow?

For sure not saying that your approach is wrong. Even if I would judge (which I don’t want to) I can’t do it properly not seeing your point.

Oh, you want to make it like a document.createElement(…) in JavaScript? Well … you would end up with some kind of DSL like in temple package, but you would immediately loose all LiveView features and “end” in:

    end
  end
end

hell … For now HEEx is best and I don’t see how it could be even improved. Maybe if Hologram would support calling any JavaScript API from Elixir then you could do it with functions, but they would require to write much more code than a DSL or template which is rather against Elixir’s 10x less LOC rule, so after all most people would see it as bad practice anyway.

If you agree with before at best you would have to write a complete HTML framework which is rather not worth especially if you would end up only with a bunch of functions to generate something that other projects like LiveView have with tons of features. Simply almost nobody would pay attention to a framework that only don’t support templates i.e. something most people are already used to. You would need a real alternative which requires a lot of time and resources and therefore most people don’t see it worth.

Even if you do so you would end up with tons of naming problems as HTML naming is very generic … For example how would you call a div function?

  1. div - is terribly bad as people would be confused with arithmetic operation
  2. element or elem - like a elem/2 for taking element from Tuple?

In fact your framework would be good only if people would always alias your module instead of import it. I don’t complain as it’s my preference, but look for all the code generated by phx generators. They use import all the time for templates, for ecto API and so on … There is lots of people just used to it and people who would not prefer other style - at least not easily …

alias MyLib.HTML

some_data
|> HTML.div(…)
# more code …

No matter if we like it or not template become a “community standard”. You would need a very, very solid API to change the way of thinking of all developers used to it and accepted it as something “normal”. As said I like aliasing more than importing as I like explicit naming in my code, so if you would work on it I may be interested.

However I’m not sure if I would change my opinion on splitting logic and template for pages. I may do a similar thing for a deisgn helper modules, so at the very end I may or may not end up doing something similar using a different API. I would say that you would have to be very, very creative to do something good on top of such old HTML naming. Unfortunately for me it does not look realistic and/or worth working on full-time for the above reasons.

For me said “barrier” was never a problem. I’m curious and so I investigated other ways, but didn’t found anything that would give me something “special”, so I would change my way of writing the code … In fact for the same reason mentioned above I rather want to have logic and template to be separated. Maybe it’s because I’m just too used to templates, but for now I don’t see anything promising enough to change my mind …

2 Likes

Are you saying that you have never performed any logic in a HEEx template? No function calls or anything like that? If so, fair enough I guess, but you must understand this is abnormal.

In practice most components and templates exhibit a mixing of concerns, and I believe this is not a failure of developers to separate their concerns into “logic” and “templates” but rather a failure of the framework to recognize that there is no such distinction.

I understand that it is on me to make my case here but I felt your response was unnecessarily dismissive. I do not want to bikeshed on this further; it’s a waste of our time.

Yes I do mean something like Temple, and I am aware it would not work with LiveView. I did mention a new framework may be a better path.

You would not want div to be a function; it would be data. Elements are a representation of DOM (and other components but let’s not get ahead of ourselves). An analog to React.createElement('div') would be HTML.create_element("div") and so on. The problem is that this is very ugly and repetitive, aesthetically, so with React they fixed this with JSX which simply compiles HTML-like syntax back into the createElement() calls.

Perhaps we could get away with something like this:

def my_component do
  ~H"""
  <ol>
    {for i <- 1..100, do: <li>{i}</li>}
  </ol>
  """
end

Which compiles to:

def my_component do
  create_element("ol",
    children: for(i <- 1..100, do: create_element("li", text: to_string(i)))
end

The difference here is that the contents of the sigil are actually Elixir code, not a template. They just have HTML mixed in. With a compiler perhaps we could do away with the sigil entirely, like JSX.