kuon
~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.
Most Liked
garrison
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.
chrismccord
I almost never use separate heex files fwiw. It’s all the same, so do what feels best for you ![]()
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 ![]()
jdiago
I’m going to approach this from a different perspective.
I had an opportunity to jump on a small-ish project a little while ago where the og author did have separate heex files. Since that’s how the project was when I got there, I just followed that convention. Although, I usually started my own LiveView code without the extracted heex files.
What caught my attention from the OP was the mention of the friction of having to jump between files so I’m looking at this with tools in mind. The reason the original question doesn’t bother me either way is because I’m used to doing this:
In the first screenshot, I have the same big file open in 2 splits so I can see 2 different regions at the same time. In the second, I have the heex file in the top split and the LiveView module in the bottom.
This is just an example but I usually try to have a max of 3(maybe 4) splits in various layouts. The splits could share the same file or not, the way I interact with them is the same.
The example I show is my nvim setup. I also do the same in vscode and zed (and JetBrains stuff back when I used those).
Popular in Discussions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #hex











