LiveView - Live Component - unmount callback

Hi!
I would like to suggest a new callback in the lifecycle of the Live Component which is unmount.

Sometimes it is nice to be able to cleanup subscriptions and such when the component is removed from the page.

An example is a “chat” component I made that subscribes to PubSub.
The component is used in a page with tabs and is only present in one of the tabs.
Being able to unsubscribe from the PubSub in the same way the subscription was made in mount would be very nice.

I think you should be able to use this terminate callback, even though it’s not guaranteed that it will get called (read why in the docs).

The official way to do this is by monitoring the process: Process — Elixir v1.17.2

The LiveComponent does not have a terminate callback as it is not a process :slight_smile:
Since it only lives in memory as a part of the parent LiveView process you never know if the component is removed or not :slight_smile:

Makes sense, if you are interested in tracking only when a component is removed.

I think the closest thing you can do is to track the html content and trigger a signal when the component was removed? Not ideal, but adding a unmount callback for a component doesn’t make that much sense either IMO.

I think it makes sense if the idea is that LiveComponent is self contained and manage its own state.
I know that you could use LiveViews instead and then get all of the features I am requesting, but the component is soooo close to have full control of its state and surrounding dependencies :slight_smile:

Also, just having all the code that relates to the usage of the component inside the component would be awesome!
In my current code I need to handle the removal of the component in the parent LiveView when the current tab change and then just call my own unmount function that lives in the component to unsubscribe to the PubSub :slight_smile:

I think not, there is a reason the decision was made to place all live_components in a single process, imagine having a page with 200 components then you will have 200+ processes for every client.

The other issue around this is the general concept of cleanup, what happens if your process crashes? This is why monitoring processes is the way to go if you want guaranteed cleanup.

Tbh I think the issue here is that you use a LiveComponent to subscribe to PubSub in the first place. Given LiveComponents are indeed not processes you’re essentially leaking a subscription onto the parent LiveView as a sideeffect, which then materializes by the fact that you cannot clean that up anymore. I agree that this is a limitation to what a LiveComponent can encapsulate, but imo the solution isn’t an unmount callback. Even with that there would still be no handle_info callback as well, so you’re still dependant on the parent LV.

1 Like

I agree :slight_smile:
But in my case the subscription would be cleaned up as the current process is allready monitored by PubSub when using PubSub.subscribe, in the case of termination.

The main reason for my suggested feature is that the parent process is not terminated when you make a change in the page that removes the component.
The LiveView process is still very much alive even though the component is not present anymore.

Since the subscription happens in the LiveComponent mount function and I forward the PubSub messages using send_update to the component, when the component is removed my code would still try to send_update to a component that is now not present anymore :slight_smile:

I offcourse handle this as described earlier but would be nice to have this as a feature that was part of the Component instead :slight_smile:

I very much agree on this!

I have another suggestion to allow PubSub to send events directly to the LiveComponent since this is the same way LiveComponent allready gets its message when using send_update.
Then the LiveComponent could be able to use PubSub without involving the parent LiveView.

This could then call update/2 in the same way send_update allready does this :slight_smile:

The way I solve the handle_info in the parent today is to use Phoenix.LiveView.attach_hook/4 function and then use send_update to forward it to the component :slight_smile:
I attach the hook by defining a hook/1 function in my component that is called from the LiveView.mount function :slight_smile:

How would you decide if a message received for being subscribed to pubsub is meant to be forwarded to the live component? Those messages are in arbitrary shape and don’t include any hint if the source for the subscription might be a child live component or the live view itself. It could even be that both are subscribed to the same pubsub.

1 Like

Well, they can with some slight tricks :slight_smile: :slight_smile:

I currently have a special module like this that handles the subscription and acts as a special dispatcher.
This does not rely on the parent LiveView and the subscription will be cleaned up when the parent LiveView process dies as normal PubSub subscriptions :slight_smile:

defmodule MyLiveComponent.PubSub do
  @moduledoc false

  def subscribe(%Phoenix.LiveView.Socket{assigns: %{myself: myself} = socket, pubsub, topic) do
    if Phoenix.LiveView.connected?(socket) do
      Phoenix.PubSub.subscribe(pubsub, topic, metadata: %{pid: self(), cid: myself})
      socket
    else
      socket
    end
  end

  def subscribe(%Phoenix.LiveView.Socket{} = socket, pubsub, topic) do
    Phoenix.PubSub.subscribe(pubsub, topic)
    socket
  end

  def unsubscribe(pubsub, topic) do
    Phoenix.PubSub.unsubscribe(pubsub, topic)
  end

  def broadcast(pubsub, topic, message) do
    Phoenix.PubSub.broadcast(
      pubsub,
      topic,
      message,
      __MODULE__
    )
  end

  def broadcast_from(pubsub, pid, topic, message) do
    Phoenix.PubSub.broadcast_from(
      pubsub,
      pid,
      topic,
      message,
      __MODULE__
    )
  end

  def local_broadcast(pubsub, topic, message) do
    Phoenix.PubSub.local_broadcast(
      pubsub,
      topic,
      message,
      __MODULE__
    )
  end

  def dispatch(entries, _dispatch_identificator, message) do
    entries
    |> Enum.each(fn
      {_pid, %{cid: cid, pid: pid}} ->
        Phoenix.LiveView.send_update(pid, cid, source: :pubsub, message: message)

      {pid, _} ->
        send(pid, message)
    end)

    :ok
  end
end

And a live component can be something like this:

defmodule MyComponent do
  use MyWeb, :live_component
  import MyWeb.CoreComponents

  @impl true
  def render(assigns) do
   ~H"""
     <%= @message %>
    <.button phx-event="send">Send</.button>
   """
  end

  @impl true
  def update(%{source: :pubsub, message: message}, socket) do
    socket
    |> assign(:message, message)
  end

  @impl true
  def mount(socket) do
    updated_socket = socket
    |> assign(:message, "No message")
    |> MyLiveComponent.PubSub.subscribe(MyPubSub, "topic")

    {:ok, updated_socket}
  end

  @impl true
  def handle_event("send", _params, socket) do
    MyLiveComponent.PubSub.broadcast(MyPubSub, "topic", "We got a message!")
    {noreply, socket}
  end