I’ve been leading a team developing an application using Elixir, Phoenix and LiveView for the last 18 months and accumulated some thoughts on the stack. For the most part, it has been a very pleasant experience.
Compared to my initial evaluation of Elixir that I’d done prior to diving in in earnest, it exceeded my expectations in terms of availability of tools & packages and the overall quality of dev experience.
Prior to Elixir, I was working primarily with Elm which probably colours my experience, but I have used a bunch of languages and have switched from static to dynamic types and back several times.
First of all, I find Elixir quite enjoyable:
As the pipeline example illustrates, Elixir seems to me to be one of those projects where instead of chasing the perfect design of every feature it’s OK to put in place pragmatic solutions even if they have warts. Some things are messy as a result but that’s an acceptable compromise:
Kernel, or things like
Set, or charlists (needed for compatibility with Erlang), or inconsistent naming (
unlessor the ability to define some custom operators, or all the different ways to access values in maps & structs that seem to be a bit confusing to newcomers.
f.()) is weird for an FP language.
My experience with Phoenix has been almost entirely with a LiveView lens, so I don’t have that much to say about Phoenix itself. It seems to be a reasonable MVC framework. The very welcome addition of compile-time checked verified routes in Phoenix 1.7 fixed one of my complaints about dealing with routes.
Another thing I don’t like is the default separation of HEEX templates from the rest of the view code. For any non-trivial views, this approach doesn’t scale and you end up with HEEX in your view code anyway, so why not keep all of the template together with the code from the beginning and benefit from improved locality?
I’m still somewhat unsure about the purpose and value of contexts. The idea is that contexts somehow provide a nice API for related functionality, sitting in front of a bunch of other modules with the actual implementation and hiding details of things like DB and external API access. There is even a longish guide about the use of contexts, but in the end it’s still unclear where and how to draw the context boundaries in practice.
For example, if I have to deal with users who have accounts which belong to companies and I want a function which gets me the stats on active accounts by company combined with each user’s last login time, should that function live in the
Accounts context? Should I even have these three contexts, or should I just have one
Accounts context? It turns out that this isn’t easy to answer.
My approach to date has been to use contexts as the effectful infrastructure layer on top of the functional core. I also try to avoid multiplying contexts unnecessarily. The idea is that there shouldn’t be one context per model but only one for each of the “central” entities in the system, so for example in the oft-used blog implementation, I might have a
Users context which deals both with users and their accounts, and an
Articles context which deals both with posts and their comments (eg if we’re only interested in getting comments for posts and not for specific users).
assigns(putting live components aside) is also a great simplification (and is a comfortable transition from Elm).
What’s not so great:
on_mounthook in addition to being defined in plug pipelines in the router. Relatedly,
live_sessionfeels like a hack that is required to enforce running through the Plug pipeline in certain situations. It feels like LiveViews actually need their own equivalent of Plug pipelines.
localStorageis also not quite trivial.
:ifetc.). It means there are two or three different ways to express logic in templates, and it’s yet another thing to learn.
Both Elixir and Phoenix are very solid tools although of course they have their share of things that I don’t agree with. LiveView is an extremely impressive addition to Phoenix that melds backend and frontend, and is just on the cusp of becoming a mature option for more complex applications.
Discussion on HN (where Jose Valim agrees with my thoughts on functional components vs live components, yay!)