Building an entire web app with Phoenix LiveView
Table of Contents
At the start of this year, I had the pleasure and opportunity of building an entire application using Phoenix LiveView. I did so without writing any JavaScript code (if we can exclude initialising date pickers on some inputs!).
Please note that in this post, I’ll be describing my personal experiences and chosen approaches while using Phoenix LiveView. This isn't a step-by-step tutorial on how to create a web app in LiveView, and each person who uses the program for similar tasks will undoubtedly take an individual approach to how they work.
This was an exciting task for me, as it was my first contact with LiveView. As I familiarised myself with the new paradigm, I found the app building process to be refreshing, as it presented me with a completely different approach to building certain features.
Not only was this an exciting and pleasant task — building an app in Phoenix LiveView was also surprisingly fast. This is largely because LiveView made it easy to build new features, and coding doesn't require switching the context between building the backend in Elixir and frontend in JavaScript. Using LiveView, I could simply focus on building the web app as a whole without any differentiation between frontend and backend.
How I approached the project
Developers typically think about the flow of communication between client and server taking the form of a mail-like conversation. That is, each request comes with a response containing the most information that we are allowed to send and that could be important for a specific page. This means that, in practical terms, we’re preloading a lot of data. (This is otherwise known as a stateless web approach.)
In LiveView, the communication exchange is more like a dynamic conversation. The client and server are conversing through the WebSocket, dynamically changing the state of a page. Because of this, I didn't need to think of GET requests, what to preload, and what should be fetched from the server. Instead, I would simply create a function inside the #{page}Live
(i.e. Projects.IndexLive
) module and bind the function to the specific element in the browser. In other words, I would be changing the value in a search input placed in a projects index to filter the projects card, keeping only ones that match the search. Thus, the entire connection between my browser and the server is stateful.
Live modules in place of controllers
Generally, I like controllers, especially when they’re well maintained and slim. But I came to like LiveView’s modules a lot more. So how did I efficiently manage them in this case? Inside the "live" folder (for LiveView modules), I created a folder for each resource that I need, just as I would create the controllers for these resources.
Then, inside these folders, I created files for every template that I wanted to use. For example:
index_live.ex
show_live.ex
new_live.ex
Okay, but what about non-GET actions? I simply treated them as events and handled them inside these modules too. So, the create
action is handled in new_live.ex
and the update
action will be handled in edit_live.ex
. This allowed me to nicely encapsulate and compartmentalise the functionality.
What if my module grows and goes from pretty, clean, and slim to “big and messy” code? I’ve encountered this issue, since my web app had some conditional parts that weren't the right fit for the context modules. Fortunately, creating the helper module and calling functions from it helped address this issue.
LiveView routers
Normally, Phoenix routers allow you to match HTTP requests to controllers' actions. It’s quite a similar case in LiveView, but instead of the controller action, it's matched to the live module and is handled with the mount/3
function.
In the router, instead of keywords like get
and post
, you use live
and point to the LiveView module. In my opinion, the result looks quite clean, especially when you’re scoping this in a way that maintains proper structure based on contexts.
Integration with custom JavaScript
As I’ve mentioned above, I didn't need to write any custom JavaScript for my web app (aside from initialising date pickers); all of the interactions were made with LiveView. That being said, there are some ideas that I have after this experience on how to improve on it — for example, I would probably try to use Vue or Alpine for certain things, like showing modals, and other things needing immediate frontend interactions or changes in DOM.
I'm currently researching this topic and it looks quite achievable, especially building this around hooks and events. To me, it’s worth it to use custom JavaScript. However, don’t reinvent the wheel if something is easily achievable with plug and play libraries, like Alpine.js.
In general, I think it’s always best to draw a proper line between what should be done with JavaScript and what should be done with LiveView. Whichever you choose, sticking with your decision throughout the app building process should help to ensure and maintain consistency.
Takeaways from the app building process
Through the process of building my app, I’ve gathered a few highlights from my experience using LiveView that I think could be useful for other developers seeking to try it out for themselves:
- Phoenix LiveView provides a way to build a dynamic and modern website without writing specific JavaScript code just for the sake of dynamicity.
- As I’ve demonstrated through this post, it's quite easy to start and become productive with LiveView if you’re already familiar with Phoenix.
- The benefits of building LiveView components over the separation between API and SPA is significant. Also, the joy and simplicity in which the same developer can address everything in a single language is a massive boon.
- Finally, it's a nice and refreshing task to try to handle common web app challenges with LiveView.