Problems with handle_params and child LiveView

Hi all

I want to reuse a full liveview on another page inside a modal.
It’s a complicated search UI with filters etc.
The idea is to provide the full UI as an alternative to a simple text based select component, if the user needs more complex filters.

This seemed like a perfect use case for a child liveview via live_render.
It works very well, but only if I remove the handle_params/3 callback from the module.
If I include it, I get this error:

** (ArgumentError) cannot invoke handle_params/3 on AppWeb.Live.Library because it is not mounted nor accessed through the router live/3 macro
    (phoenix_live_view 1.0.12) lib/phoenix_live_view/route.ex:26: Phoenix.LiveView.Route.live_link_info!/3
    (phoenix_live_view 1.0.12) lib/phoenix_live_view/channel.ex:1209: Phoenix.LiveView.Channel.verified_mount/8
    (phoenix_live_view 1.0.12) lib/phoenix_live_view/channel.ex:84: Phoenix.LiveView.Channel.handle_info/2
    (stdlib 6.2.1) gen_server.erl:2345: :gen_server.try_handle_info/3
    (stdlib 6.2.1) gen_server.erl:2433: :gen_server.handle_msg/6
    (stdlib 6.2.1) proc_lib.erl:329: :proc_lib.init_p_do_apply/3

I do not need the handle_params callback when it’s rendered as a child liveview, but when rendered at the root I do need it. (The view has tabs which change the URL, but when rendered as a child liveview I disable those tabs)

Why can it not just ignore the callback when rendered as a child view? Why does it even try to invoke it?

How can I solve this?
I’m currently thinking about writing a macro which essentially duplicates my whole module except for handle_params
But there has to be a better solution, right?

1 Like

Factor out the functionality into either a LiveComponent or a child LiveView and then compose it: render it as a child of the LiveView which has the handle_params/3 callback. Then when you don’t need the params, render it as a child of a LiveView which lacks that functionality (or if the child is itself a LiveView, maybe render it directly).

I am partial to using LiveComponents for composition instead of composing LiveViews (we had a thread about this recently), but either approach would solve your problem.

Thanks, but my view uses a handle_info hook, so factoring it out into a LiveComponent doesn’t work. Also, I don’t like that when putting things into a LiveComponent, I need to add phx-target={@myself} everywhere and also pass it down into functional components.

Using a nested liveview for the main UI is an interesting idea, but also doesn’t work, as I can’t pass down those params from the parent to the child. See e.g. Accessing the URL in a child LiveView.

But realizing that I’m already using hooks, made me find another approach that sadly doesn’t work:
Use a hook to conditionally attach a handle_params callback:

socket |> attach_hook(:handle_url, :handle_params, &handle_url/3)

That seems like it should work, but when calling push_patch I’m running into:

** (UndefinedFunctionError) function AppWeb.Live.Library.handle_params/3 is undefined or private

Adding a dummy callback makes it work again, but then I’m back to square one as then I can no longer embed it :face_with_diagonal_mouth:

def handle_params(_params, _session, socket), do: {:noreply, socket}

This is very frustrating, the presence or absence of a single line of code prevents this from working.


I got it working without needing handle_params by switching out push_patch with push_navigate.
This is not a good solution, as it now obviously no longer preserves scroll position when switching tabs.
Despite being a UX downgrade, I think I’m going with this workaround for now.
Unless someone can find a better way?

1 Like

You could put the handle_info on the parent LiveView and then forward the events with send_update. I agree this is not ideal, but that’s probably the solution I would go with given what you’ve said.

A clever idea, but unfortunately as you’ve discovered the hooks and callbacks are rather tightly coupled in the implementation (I had noticed this while tracking down the source of your error). Halting the hook would even stop the real callback from being invoked, but tragically the callback presence is checked first.

It would be nice if that check was done lazily. Maybe consider opening an issue?

You could put the handle_info on the parent LiveView and then forward the events with send_update.

Yeah, this could work when it’s embeded, but what about the push_patch when it’s not? This would then somehow need to be pushed to the outer LiveView. This involves so much wiring, I do not like it.

It would be nice if that check was done lazily. Maybe consider opening an issue?

Not sure if it’s really a valid issue, as it seems I’m going up against the “proper” way of doing things anyway.


Anyway, I’ve found another solution and I’m actually quite happy with this one. Just duplicate the module without handle_params and delegate to the main one for the rest:

defmodule AppWeb.Live.Library do
  ...

  defmodule Embed do
    alias AppWeb.Live.Library
    use AppWeb, :live_view

    @impl true
    defdelegate mount(params, session, socket), to: Library
    @impl true
    defdelegate handle_event(event, params, socket), to: Library
    @impl true
    defdelegate render(assigns), to: Library
  end
end

And then I can live_render(@socket, Library.Embed, args) without any issues.
It’s so simple, I don’t know how I didn’t think of this first!

With this, the main view can easily do push_patch and handle the url changes and the embedded version just works without any changes needed.

2 Likes