Handling PubSub messages from within a LiveComponent?

Hello!

I have a LiveView that subscribes to a Phoenix.PubSub topic and shows some logs in real time. Now I want to move this part into a stateful LiveComponent, so that all logic regarding subscribing and reacting to messages in the topic lies within it.

But, I think this can’t be done - I can subscribe to the topic from within the LiveComponent, but it’s the parent LiveView process who is going to receive the messages, so I still need to have the handle_info logic in the parent LV process.

Is there something similar to phx-target="@myself" that can be used for PubSub like it’s done for events, so I can achieve this?

1 Like

The parent LV will always handle the messages, and then you can use those PubSub event handlers to pass the relevant changes to the child component(s).

2 Likes

Thanks, that’s what i thought… it would have been great to be have a LiveComponent that could be embedded into different LiveViews without needing them to know how to handle those messages!

2 Likes

Hey folks, hi ! I reply to this topic as this is the issue that I have bumped into in a side project I am working on.

I have a website with some stateful components defined, like Navbar. Now, this component has a part which is rendered regardless any authentication state, and another one which is authentication dependent. This means that to an authenticated user a Logout button (among others) will be shown. Now, I have two options: I could potentially implement in the component the logout logic, or send a message to the parent which has to implement the logic, update the assigns, and by passing it to the Navbar component, update the page as a whole.

My issue with this is that in the latter scenario, every page which renders the component must know what to do to implement the logout; the former scenario looks the most reasonable, but I was trying to isolate the action associated to an event elsewhere, so to implement a full decoupling. Why should I do this ? Because I might reuse the component in another project where the logout meaning would potentially be different, and just leveraging messages and communication to trigger specific implementations to be pulled in and take care with super clear responsibility would be ideal, I think.

So I was thinking to let component to publish the event on pubsub; another component (for example, AuthenticationManager) would consume the message to execute proper logic; on the other side, any page which is rendering the Navbar component subscribes to a topic (for example, sessions:<user_id>) that could receive messages like {:logged_out, <user_id>} from the AuthenticationManager and simply update the assigns to propagate the change.

Anyway, apparently, this isn’t doable because of the parent views catching whatever message. Is this correct ? Is implementing the logic in the component the only way to do this ? And if this is true, is it correct to affirm that any page using the Navbar component should implement logic for any message that the component would generate ?

Sorry, I am just trying to avoid redundances.

If I understood correctly, you can use a LiveView on_mount hook, that subscribes to the events, uses attach_hook on the handle info callback, and then dispatches the relevant events to the component. This is what we do for Livebook’s sidebar: https://github.com/livebook-dev/livebook/blob/fa7244eae057499cb7088b83740977bfb7ce3fb1/lib/livebook_web/live/hooks/sidebar_hook.ex

It may make sense to allow the component to subscribe to the parent events, but one can imagine how this can become very confusing and hard to debug!

3 Likes

Hi @josevalim , the communication flow would be exactly the opposite.

  • components can send events to third parties entities (that know what to do when those events happen) via pubsub topic
  • liveview pages can subscribe to a pubsub topic
  • third party entities can subscribe to a topic to receive messages from components, execute some logic and send events to pages

The problem arises with the third party entities, which can’t receive message from components, that are instead received (apparently by design) by the component’s parents.

I haven’t yet read your link, I just wanted to specify (hopefully) better what’s my use case.

Hey - I am replying here because I can’t start a topic on my first post. Plus I keep getting sent to the same unresolved questions on various forums.

I wanted to share what I have done here, (which is admittedly a bit of a hack) to get around this issue with LiveComponents not being a process.

I won’t share tonnes of context unless people want more, but the basic thing is this:
I have a particularly complex component - (a media picker/uploader), which is used in about 20 liveviews and will need to go to more. It is used on forms, usually in dynamic fields for associations. Therefore the logic really needs to be encapsulated inside the LiveComponent.

I can’t use a LiveView because you can;t pass initial and updated props to a LiveView when using live_render. This component has many contextual options which need to be passed on initialisation. The owner of the media, the media type etc. Media can be owned by a company and accessible by all members of the company, or by a customer, who is the only one with access until added to a public entity…

During the process of uploading, displaying and selecting media inside this component there are a bunch of background processing jobs that happen (uploading via tus/uppy, encoding videos, token generation, thumbnail generation etc) and need to be reported back to the component. Again - this is used in many places, so attaching hooks and implementing logic in mutiple liveviews doesn’t feel right. The logic for the component should live IN the component.

Up until now, I have been attaching a phx-hook to the root element in the component. Then when job status updates occur, I send a message to a phx channel channel, which sends a message to the channel socket and then the js socket client sends a message back to the mounted hook.

The above solution works, but it is messy, difficult to trace logic and hard to debug. As the complexity of the application grows, it gives me the itch! :smiley:

So I have started using another approach: Running a lightweight GenServer in my component.


defmodule MyApp.MyLiveComponent do
  use MyApp, :live_component
  use GenServer
  ....
end

Then when the component mounts and connects to the liveview, I start the GenServer, link it to the parent liveview process and pass the socket into the init of the GenServer.


 if connected?(socket), do: {:ok, pid} = start_link(socket)

Now my GenServer can subscribe to pubsub events. GenServers have a handle_info, but LiveComponents don’t.

  def init(socket) do
    {:ok, socket, {:continue, :subscribe}}
  end

  def handle_continue(:subscribe, socket) do
    Phoenix.PubSub.subscribe(MyApp.PubSub, "test-my-topic")
    {:noreply, socket}
  end
 

When the component server receives the message


def handle_info(message, socket) do
    send_update(socket.root_pid, socket.assigns.myself, message)
    {:noreply, socket}
  end

Myself is the CID of the component, which is how the liveview tracks the livecomponents it is managing.

I’ll abstract this away into a macro and now I can just add the functionality to any LiveComponent that needs it.

I want to stress though - it is very rare that you ever need something like this. Most of the time, LiveComponent, Component and LiveView fit the bill, but sometimes there is a gap in the functionality where components need to receive events from external processes.

Hope it helps somebody else who is pulling their hair out like I was :smiley:

4 Likes

Hi!

This was a very smart approach to the problem!
I also have a similar approach just not using new processes: https://elixirforum.com/t/liveview-live-component-unmount-callback/66050/11?u=mortenlund

But i think you also have the same problem i have:

What to do when the livecomponent is removed from the page while the liveview still lives on.

This will not cleanup the subscriptions from the genserver until the parent liveview also dies.
This results in the genserver in your case still calling send_update to the component that no long exist.

I have the same issue with my approach.

Did you figure out a smart way to handle that case?

Sorry, In my case that isn’t really an issue. The component(s) mount when the live view mounts and share the same lifespan as the live view. A “select media” button is always visible for each component instance. They receive updates in the background which are then available when the media modal is opened again. The only question is whether the modal is visible or not, which is controlled by that button.

I have a lot of isolation between live views due to complex authorisation rules.

That said, whichever action removes your live component can send a a terminate message to the genserver if it’s a problem you need to solve.

Yeah, this is what i do today :blush:
Would be awesome if the liveview had just a tiiiny bit more self-control and could handle both its state but also its interaction with other processes :slight_smile:
I feel like this is a design choice and not a technical limitation, so would be nice to investigate what is limiting the ability to get a «unmount» event or something similar for the live_component :slight_smile:
Or, be able to give assigns to live_views using live_render. Currently only a session map can be provided and it mush be serializable :slight_smile:

You can use assign_new to share loaded records by only putting ids on the session map the same way you can do so between the plug pipeline and LV on the static render.

Honestly, this component is the only time I have ever felt the need to break out of the current patterns. Just because of the frequency of use, the complexity and the isolation required between component instances.

The joy of Elixir is that there is always 10 ways to solve a problem. You can pick the right one for the task at hand. If your component state lives longer than your live view, you should probably just use a dynamic supervisor that spins up a gen server and an agent/registry. Then you can control the state from anywhere. You could also store cross view/component state in ETS or Mnesia depending on the level of persistence required.

I tend to use Cachex for simple stuff like this just because it is a nice friendly wrapper with a sensible API.

It doesn’t take much to spin up a state machine per user with a dynamic supervisor. I use these a lot for dynamic processing pipelines that need to adjust their steps depending on data query results or api responses.