Lately I’ve been thinking about how to organize components as a LiveView application grows. One of the pain points I’ve found (for myself and beginners) is communicating over PubSub with LiveComponents. The only way to do so is through the parent LiveView, which works well but introduces boilerplate and some indirection, as the LiveView is required to maintain the PubSub subscriptions, and communicate state changes via component assigns or send_update/2
.
It would be nice if LiveComponents could implement a handle_info/2
callback, and had a way to manage their own PubSub subscriptions. Well, I was able to implement just that. This is how the “connected” component itself looks:
defmodule CounterComponent do
use AppWeb, :connected_component
def render(assigns) do
~H"""
<div {@connected_attrs}>{@count}</div>
"""
end
def on_mount(socket) do
process_setup = fn ->
Phoenix.PubSub.subscribe(App.PubSub, "inc_channel")
end
{:ok, assign(socket, :count, 0), process_setup}
end
def handle_info(:inc, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
end
I’ve called it “connected” because the component has its own process, which is connected to it via the parent LiveView process. In the on_mount/1
callback, the component returns an anonymous function which is called in a new process and creates any interprocess connections needed by the component (generally, PubSub subscriptions).
In this example, the component receives :inc
messages via inc_channel
, which when received increments the :count
assign on the component. The advantage of this is that the LiveComponent is now fully encapsulated, including its interactions with PubSub.
You can find a complete example and demo video here: GitHub - jtormey/connected_component_demo: Phoenix LiveView project demoing the ConnectedComponent concept
The implementation itself is here (not extensively tested, there may be bugs).
Using these components is transparent to the parent LiveView and even allows for deeply nested components to have their own handle_info/2
callbacks. It just requires an on_mount/1
handler to be called in the parent LiveView, which can be applied for all LiveViews globally by calling it in the live_view/1
macro in your web module (you can see how this works in the demo application).
There is a slight performance cost: an additional process for each component that uses this pattern, and each message that is received is copied one more time than would normally be necessary. Though I think these can be justified.
I’m curious if anyone would find this useful in their own applications. Personally I like the convenience and the API, and wonder if something like this might be a worthwhile addition to LiveView itself.