How to refactor handle_info/2 in LiveView

In lib/example_web/live/ I have multiple LiveViews which contain a couple of identical handle_info/2 functions. I would like to refactor those into one file to be imported into them. My Elixir knowledge is too flat to find a solution for this. I would appreciate some advise:

  • Can I refactor the code in the way I’d like to do it?
  • Which directory would be appropriate for the new file?
  • How is the naming convention for that file? If there is no convention: What makes most sense?
  • Is import the right way to go?

I am aware that these are super basic questions. It is embarrassing to ask them. But when ever I google for a solution I find “it’s easy” or “just put it anywhere”. But not a single example of somebody who actually refactors LiveViews which share the same handle_info/2 functions. Thank you for your help.

Here’s an example LiveView I am working with:

defmodule ExampleWeb.PageLive do
  use ExampleWeb, :live_view

  alias Example.Presence
  alias Phoenix.Socket.Broadcast

  @impl true
  def mount(_params, _session, socket) do
    Phoenix.PubSub.subscribe(Example.PubSub, "room:lobby")
    {:ok, _} = Presence.track(self(), "room:lobby", UUID.uuid4(), %{status: "user"})

    socket =
      assign(socket,
        current_users: current_user_ids()
      )

    {:ok, socket}
  end

  @impl true
  def handle_info(%Broadcast{event: "presence_diff"}, socket) do
    socket =
      assign(socket,
        current_users: current_user_ids()
      )

    {:noreply, socket}
  end

  @impl true
  def render(assigns) do
    ~L"""
    <%= inspect(@current_users) %>
    """
  end

  defp current_user_ids() do
    Presence.list("room:lobby")
    |> Enum.filter(fn {_id, x} -> hd(x.metas).status == "user" end)
    |> Enum.map(fn {k, _} -> k end)
  end
end

I would like to share the same handle_info/2 with multiple other LiveViews.

To actually share the function definition you’d need macros/metaprogramming in elixir. import doesn’t actually export those imported functions for the module imported to, but only makes those functions available to be used by the module itself.

I personally do not like the indirection metaprogramming would bring to the table here, so I’d suggest just sharing the implememtation instead of the function definition:

defmodule ExampleWeb.PageLive do
  use ExampleWeb, :live_view

  alias Example.Presence
  alias Phoenix.Socket.Broadcast
  alias ExampleWeb.PresenceHelper

  […]
  
  @impl true
  def handle_info(%Broadcast{event: "presence_diff"} = event, socket) do
    socket = PresenceHelper.handle_presence_diff(socket, event)
    {:noreply, socket}
  end
end

This approach becomes even more preferred as you want to compose multiple function heads for handle_info/2 with implementations placed in multiple other modules. This is where a macro approach often becomes tedious for jugling composition.

I’m aware that this does not actually share handle_info/2, but I do think this is even better this way. It’s still explicit within each liveview how incoming messages are handled – even that they’re handled in the first place. Each liveview can make its own decisions about precedance and pattern matching, while the bits, which are actually problematic to copy are moved into one common helper module.

2 Likes

I agree with @LostKobrakai here. However, I suspect this is another approach you are trialling to allow use of a LiveComponent for displaying current users, in which case using a macro to inject handle_info\2 with a specific pattern may not be so bad.

If you take a look at your <App_name>_web.ex file in your lib folder you will see a couple of examples for injecting import and alias statements. The basis theory is here: Macros - The Elixir programming language

So the handle_info function definitions would be injected using something like:

use PresenceMacroMayhem, :inject_change_handler

and your macros would be defined more or less like:

defmodule PresenceMacroMayhem do
  def inject_change_handler do
    quote do
      @impl true
      def handle_info(%Broadcast{event: "presence_diff"} = event, socket) do
        socket = PresenceHelper.handle_presence_diff(socket, event)
        {:noreply, socket}
      end
    end
  end
end

Sorry about all the edits - got a new, ultra-sensitive mechanical keyboard and I kept hitting the Enter key by mistake

1 Like