The Pug Automatic

Communicating between LiveViews on the same page

Written August 1, 2020. Tagged Elixir, Phoenix LiveView.

If you have multiple LiveViews on the same page, it's perhaps not obvious how they can communicate with one another. This post describes a few ways.

First off, consider whether you want to use multiple LiveViews at all, or if a single LiveView containing components would be more suitable. (In my case, I went with multiple LiveViews so each could be more self-contained, with its own timers and so on.)

I'm using LiveView 0.14.4.

Parent to child via :session and :id

A parent LiveView can pass "session" data to a child LiveView:

lib/my_app_web/live/parent_live.ex
def render(assigns) do
~L"""
<%= live_render @socket, MyAppWeb.ChildLive,
id: :child,
session: %{"hello" => @world}
%>
"""

end

But it's only passed once, when the child is mounted. If the world assign is later changed in the parent, the child won't update automatically.

We can fix this by including the assign in the child's ID:

lib/my_app_web/live/parent_live.ex
def render(assigns) do
~L"""
<%= live_render @socket, MyAppWeb.ChildLive,
id: "child_#{@world}",
session: %{"hello" => @world}
%>
"""

end

Now, whenever the ID changes, the child will be unmounted and remounted.

A downside to remounting is that all of the child's state will be reset – we're not just updating the world value and leaving everything else as-is.

Note that you may need to do id: "child_#{inspect(@some_assign)}" depending on its type.

Child to parent via send

Because each LiveView is a process, you can send messages between them, as long as you know the PID.

Conveniently, the socket contains the parent_pid, so sending a message from a child LiveView to its parent LiveView is easy:

lib/my_app_web/live/child_live.ex
# Let's assume this is triggered by clicking some link.
@impl true
def handle_event("say_hello_to_parent", _params, socket) do
send(socket.parent_pid, {:hello, "world"})
{:noreply, socket}
end
lib/my_app_web/live/parent_live.ex
@impl true
def handle_info({:hello, message}, socket) do
IO.inspect message
{:noreply, socket}
end

There is also a root_pid to access the root LiveView, if they're nested more deeply.

Both parent_pid and root_pid are documented in the typespec, which means it's fine to rely on them – they're part of the public API.

Parent to child via send

The socket doesn't include child PIDs out of the box, but we can have children send their PIDs to the parent on connected mount:

lib/my_app_web/live/child_live.ex
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
send(socket.parent_pid, {:child_pid, self()})
end
end

And the parent can store it:

lib/my_app_web/live/parent_live.ex
@impl true
def handle_info({:child_pid, pid}, socket) do
{:noreply, assign(socket, child_pid: pid)}
end

Now the parent can send messages to the child:

lib/my_app_web/live/parent_live.ex
# Let's assume this is triggered by clicking some link.
@impl true
def handle_event("say_hello_to_child", _params, socket) do
send(socket.assigns.child_pid, {:hello, "world"})
{:noreply, socket}
end
lib/my_app_web/live/child_live.ex
@impl true
def handle_info({:hello, message}, socket) do
IO.inspect message
{:noreply, socket}
end

Be mindful of the timing here – some callbacks in the parent (like handle_params) may happen before the child PID is known.

If you try to send to a child PID after it has been unmounted, it will silently do nothing. (Just like sending to any PID where the process is no longer alive.)

Sibling to sibling with a shared ancestor via send

What about two sibling LiveViews?

If they share an ancestor LiveView, we can use a variation on the previous technique:

The children send their PIDs to their parent or the root, which stores them.

Child 1 can then send a payload like {:tell_child_2, {:hello, "world"}} for the parent or root to pass on:

lib/my_app_web/live/parent_live.ex
@impl true
def handle_info({:tell_child_2, message}, socket) do
send(socket.assigns.child_2_pid, message)
{:noreply, socket}
end
lib/my_app_web/live/child_2_live.ex
@impl true
def handle_info({:hello, message}, socket) do
IO.inspect message
{:noreply, socket}
end

Anything with a shared root LiveView via PubSub

Alternatively, we can use PubSub to communicate between anything on the same page (whether siblings, ancestor/descendant, or cousins twice removed), as long as they have a shared root LiveView.

See the PubSub docs for how to set it up. At the time of writing, you just need to add it to your supervision tree.

For the purposes of this blog post, we will restrict PubSub to updates within the current page. If you want to send some update for every user (e.g. new messages in a chat room), or every tab/window opened by the current user, PubSub can do that too.

We'll use the socket's root_pid in the PubSub topic as a way of uniquely identifying the current page. Two sibling LiveViews without a shared ancestor will each have their own PID as their root_pid, so this wouldn't work.

Anyone who wants to receive messages can subscribe on connected mount, and set up a handler:

lib/my_app_web/live/child_1_live.ex
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "page_#{inspect(socket.root_pid)}")
end
end

@impl true
def handle_info({:hello, message}, socket) do
IO.inspect message
{:noreply, socket}
end

Then any other LiveView with the same root_pid can send messages:

lib/my_app_web/live/child_2_live.ex
# Let's assume this is triggered by clicking some link.
@impl true
def handle_event("say_hello_to_page", _params, socket) do
Phoenix.PubSub.broadcast_from!(MyApp.PubSub, self(), "page_#{inspect(socket.root_pid)}", {:hello, "world"})
{:noreply, socket}
end

(By using broadcast_from!/5 rather than broadcast!/4, the sending process won't itself receive the broadcast even if it's a subscriber.)

You could probably use the process registry instead of PubSub, but process registry names must be atoms, which aren't garbage collected, so it isn't advisable – each page would use a bit more memory that is never reclaimed, and you might eventually hit the atom limit. Also, unlike PubSub, this process registry only works on the local node.

I assume that PubSub will use more resources than just relying on send, especially on a high-traffic site, since it keeps track of subscribers, but I don't have the numbers. If you measure it, let me know.

Anything (without needing a shared root) via PubSub

If you have a user ID or session ID, you could use that with PubSub instead of the root_pid… but if the same user has multiple tabs or windows open in the same browser, all those windows would be affected – not just the current one.

To target only the current page, you could generate a unique per-page identifier and use that in the PubSub topic:

lib/my_app_web/controllers/my_controller.ex
def show(conn, _params) do
# Assuming you use Ecto.
page_id = Ecto.UUID.generate()

render(conn, :show, page_id: page_id)
end
lib/my_app_web/templates/my/show.html.eex
<%= live_render @conn, MyAppWeb.OneLive, session: %{"page_id" => @page_id} %>
<%= live_render @conn, MyAppWeb.TwoLive, session: %{"page_id" => @page_id} %>
lib/my_app_web/live/one_live.ex
@impl true
def mount(_params, %{"page_id" => page_id}, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "page_#{page_id}")
end
end

And so on.

That's it!

I'll leave it to the reader to determine which of these techniques, if any, is most suitable for your use case.

As always, I'm very happy to receive feedback either in the comments or on Twitter!