Best way to handle duplicated functions between two live views

I have two live views where the only differences are one function and the template.

defmodule PetLive.Index do
  ...

  def mount(_params, _session, %{assigns: %{current_user: current_user}} = socket) do
    {:ok, assign(socket, :pets, list_pets(current_user))}
  end
  
  ...

  defp list_pets(user) do
    Pets.list_all_pets(user)
  end

end

and

defmodule PetLive.MyPets do
  ...

  def mount(_params, _session, %{assigns: %{current_user: current_user}} = socket) do
    {:ok, assign(socket, :pets, list_pets(current_user))}
  end

  ...

  defp list_pets(user) do
    Pets.list_pets_for_user(user)
  end
end

All the rest of the functions are pretty much the same : handleParams/3, four definitions of apply_action/3 and one definition of handleEvent/3. This code is modified from the code generated by mix phx.gen.live.

What is the idiomatic way to de-dupe this code? I’m thinking maybe I want to add a macro so each module just uses the macro and then defines its own list_pets/1 callback. If so, does anyone have any good resources to point me to? Haven’t had to add any macros before.

Thanks

It’s generally recommended to avoid macros unless there’s no other way. In this case I would suggest something like:

defmodule Pets.HelpersLive do
  def mount_pets(load_fun, _params, _session, %{assigns: %{current_user: current_user}} = socket) do
    {:ok, assign(socket, :pets, load_fun.(current_user))}
  end
end

defmodule PetLive.Index do
  def mount(params, session, socket), do: Pets.HelpersLive.mount_pets(&Pets.list_all_pets/1, params, session, socket)
end

defmodule PetLive.MyPets do
  def mount(params, session, socket), do: Pets.HelpersLive.mount_pets(&Pets.list_pets_for_user/1, params, session, socket)
end
1 Like

You could put the common parts in a LiveComponent and then each of PetLive.Index and PetLive.MyPets could render that component with different pets.

1 Like

Or have a single liveview. Depending on how you navigate there, set some kind of mode assign (probably not a good idea if you want to hide all pets from non-admin users, in which case @al2o3cr’s approach would be better).

  defp list_pets(socket) do
   user = socket.assigns.current_user
   mode = socket.assigns.pet_listing_mode

   case mode do
     :all_pets -> Pets.list_all_pets(user)
     :just_current_users_pets -> Pets.list_pets_for_user(user)
    end
  end

I think this might be the best approach. Then each live view would pass in a function for how that view will list the pets, the markup of how to render the differences for each view, as well as handle any differences that diverge between the two lives views.

Beware trying to make things overly-generic in this situation - if there are that many differences between the pages, what you likely have are two similar LiveViews that both use a common set of UI components.

For instance, I’d recommend you not try to DRY out a short function like:

  def mount(_params, _session, %{assigns: %{current_user: current_user}} = socket) do
    {:ok, assign(socket, :pets, list_pets(current_user))}
  end

My reasoning is that this function’s contents are likely to diverge from other copies as the LiveView grows in complexity.

1 Like