First off, hello! I’m Alex Bruns. This is the first of many posts about programming I intend to write. If you enjoy it and would like to see more, subscribe.
For my inaugural post, I wanted to write something short and sweet while still providing practical value. So, let’s talk about data loading.
The primary selling point of most LiveView applications is the ability for them to be reactive to their system’s data changing “underneath” them. E.g. a message board that automagically shows new messages as they’re sent, or an order book that updates bids and asks in realtime.
Live data is basically table stakes in my opinion. If your live view isn’t “live” it’s just an MPA with marginally faster navigation and a lot more server resource usage.
So, it stands to reason LiveView developers have worked to make “live” data as easy to work with as possible. Right? Well, sort of. As far as I can tell, here’s what 90+% of LiveViews are doing today:
defmodule AppWeb.Good.Live do
use Phoenix.LiveView
@impl Phoenix.LiveView
def render(assigns) do
...
end
@impl Phoenix.LiveView
def mount(_params, _session, socket) do
:ok = PubSub.subscribe(App, "users")
{:ok, assign(socket, :users, App.Users.all()}
end
@impl Phoenix.LiveView
def handle_info({:user, :updated, _}, socket) do
{:noreply, assign(socket, :users, App.Users.all()}
end
end
I’m not going to rag on this too much considering what I’ve been through with other frameworks, but I hope we can all agree there’s room for improvement. Sure, there’s the simple stuff like hiding the subscribe call in a context function, extracting the data loading into its own function, using structs for easier pattern matching, etc. but that’s all just surface level. For completeness, here’s what a “nice” version of the above might look like:
defmodule AppWeb.Better.Live do
use Phoenix.LiveView
alias App.Users
@impl Phoenix.LiveView
def render(assigns) do
...
end
@impl Phoenix.LiveView
def mount(_params, _session, socket) do
:ok = Users.subscribe()
{:ok, assign_users(socket)}
end
@impl Phoenix.LiveView
def handle_info(%Users.Event{}, socket) do
{:noreply, assign_users(socket)}
end
defp assign_users(socket) do
assign(socket, :users, Users.all())
end
end
It’s better, but I still have issues (with the code, not just in general). Specifically, if I want to use my live users data in another LiveView I have to copy-paste a lot of code, and if I want to handle edge cases like different live data sources using the same subscriptions, or debouncing data revalidation things only get worse.
My answer to these issues was actually to write a library that handles this and a lot more (more on this in a future post), but for this post I’d like to share a simple solution that can get you surprisingly far: life-cycle hooks.
def App.LiveUsers do
import Phoenix.Component
import Phoenix.LiveView
alias App.Users
def on_mount(:default, _params, _session, socket) do
{:cont,
socket
|> assign(:live_users_attached?, false)
|> attach_hook(__MODULE__, :handle_info, fn
%Users.Event{}, socket
when socket.assigns.live_users_attached? ->
{:halt, assign(:users, Users.all())}
%Users.Event{}, socket ->
{:halt, socket}
_msg, socket ->
{:cont, socket}
end)}
end
def attach(socket) do
unless socket.assigns[:live_users_attached?] do
:ok = Users.subscribe()
socket
|> assign(:users, Users.all())
|> assign(:live_users_attached?, true)
else
socket
end
end
def detach(socket) do
if socket.assigns[:live_users_attached?] do
:ok = Users.unsubscribe()
assign(socket, :live_users_attached?, false)
else
socket
end
end
end
With the above we can rewrite our example like so:
defmodule AppWeb.Best.Live do
use Phoenix.LiveView
on_mount App.LiveUsers
@impl Phoenix.LiveView
def render(assigns) do
...
end
@impl Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, App.LiveUsers.attach(socket)}
end
end
Not only does the above Just Work™️, its also portable between live views, and handles dynamic live data with ease (e.g. live data that is only used part of the time that a live view exists).
Finally, because this allows us to hide our implementation details we can shove a lot more complexity into this code without making our LiveView more complex. For example, how might you handle two live data sources that rely on the same subscription topic? It’s not simple, but at least with this approach it’s hidden, and it’s that nearly as good?
Same with performance. Need to debounce live data revalidation to prevent tipping over your database? No need for your LiveViews to know how that works.
So, to sum up, LiveView is great, live data is hard, life-cycle hooks are great, and you should use them for your live data.
Now, before you go, I gotta do the thing. Please subscribe if convenient. If inconvenient, please subscribe all the same.
Finally, in honor of Donald Knuth, I plan to offer a bounty for any typos found in my writing. Here’s how this will work. If you find a typo in a post of mine, please leave a comment describing the typo. I will fix typos as they are reported, and I will note typo reporters at the end of each of my posts. I’m not currently making any money off of this blog, so I can’t offer any cash rewards for now, but who knows, this may change in the future.