A More Perfect Program

A More Perfect Program

Share this post

A More Perfect Program
A More Perfect Program
LiveView data loading

LiveView data loading

It's doesn't have to be (as) painful

Alex Bruns's avatar
Alex Bruns
May 22, 2023
1

Share this post

A More Perfect Program
A More Perfect Program
LiveView data loading
Share

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.

1

Share this post

A More Perfect Program
A More Perfect Program
LiveView data loading
Share
© 2025 Alex Bruns
Privacy ∙ Terms ∙ Collection notice
Start writingGet the app
Substack is the home for great culture

Share