How to access the :id in mount/1 for a LiveComponent

I have a chat app I’m building to test liveview. Right now I’m loading everything in a single liveview. All the messges, etc. When a message changes, I broadcast to chat:id and listed for it, then I map over the messages, and update them if the change is specific to a message. This leads to a lot of re-render and seems inefficient. If I use temporary_assigns: [messages: []], then an update to a message (i.e. it get’s liked), doesn’t work since the message no longer exists.

My plan is to instead only listen to chat level changes in the main LiveView, and render N MessageComponents in a LiveView.Component. In each component’s mount, I’ll subscribe to message:id, and then broadcast message changes that way. I’m hoping this will lead to less re-rendering of the entire message list. Right now a message like leads to an entire re-render of the message list, and not just the single message component. The issue is, the only assigns in the socket once the components mount is called are: assigns: %{flash: %{}, myself: 0},.

Here’s some of the code:

View:

<%= for message <- @messages do %>
  <%= live_component @socket, MyAppWeb.MessageComponent, Map.merge(assigns, %{id: message.id}) %>
<% end %>

MessageComponent

defmodule MyAppWeb.MessageComponent do
  def render(assigns) do
    MyAppWeb.ChatView.render("message.html", assigns)
  end
  
  def update(assigns, socket) do
    socket =
      assign(socket, %{
        id: assigns.id,
        current_user: assigns.current_user,
        chat: assigns.chat,
        message: assigns.messages |> Enum.find(&(&1.id == assigns.id)),
      })

    {:ok, socket}
  end
  
  def mount(socket) do
    if connected?(socket) do
      # This part doesn't work, as there's no `:id` in assigns
      MyAppWeb.Endpoint.subscribe("messages:#{socket.assigns.id}")
    end

    {:ok, socket}
  end
end

My questions are:

  • How can I access the :id in the component’s mount call?
  • Will this be a more efficient way to do this? Since my messages are “rich messages” i.e. more than just displayed text, but they have their own events (likes, replies, etc.) that need to be listened to, is it actually more efficient to handle this state in a separate component, rather than handling all the chat and messages state in the main LiveView module?

Thanks!

1 Like

LiveComponents don’t implement the handle_info callback that you want to use so your desired approach of subscribing to the message:id topic from the component won’t work.

Edit: maybe you’re already aware about components not implementing the callback but want to subscribe to the topic from the component and listen to it from the parent LV. Still, what I say next is valid. Check the demo and let us know if you need assistance!

Take a look at the LiveView video demo (https://www.phoenixframework.org), you will see they do exactly what you want. The app uses temporary_assigns to render the posts and yet it is able to update when a post is liked.

2 Likes

I took a look at that demo. Unfortunately it’s not quite the same scenario, as there they just have the like_count as a field on the post. In my case I also have a separate table that tracks user likes, and the like appears slightly differently whether or not the current user liked the post or not. I think for now I’ll stick with doing it without LiveComponents.

Could you tell us why the socket passed to mount/1 does not provide access to any (readonly) data ?
(“parent” assigns, connect info, connect params)

The assigns in the new socket are completely overwritten.
The “original” assigns are lost and not accessible while they could be.

mount_component

It seems that the “parent” will always be the LiveView, never the actual parent in case of a nested component.
A nested component would want to access the state of the parent LiveComponent in mount/1.