jtormey

jtormey OP

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 · GitHub

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.

First 10 of 25 Posts Switch mode

gushonorato

gushonorato

You can achieve the same goal by creating nested LiveViews.

In my opinion, the primary reason to use components is to save resources by avoiding the need to spawn a new process. I don’t see a compelling reason to use a “connected component” since LiveView already provides that functionality.

cmo

cmo

Did you explore adding handlers in on_mount for the components without the extra process? If so, what made you go this route?

olivermt

olivermt

A major difference is that passing props to a nested liveview and updating it on-page (outside of messages) is much more clunky.

That said I am not convinced the upside is bigger than the added downside/complexity.

jtormey

jtormey OP

Nested LiveViews are similar, but they are clunkier and even more expensive:

  • They also spawn a new process, which is even heavier than my connected component process (it holds the state of the LiveView, instead of just passing along messages).
  • The nested LiveView can’t receive assigns directly from its parent, so it has to re-mount all of the state it needs, including I believe your whole auth pipeline.
  • It’s more isolated from the parent LiveView than a LiveComponent, making it more difficult to relay information and making the dev experience more difficult in my experience.
LostKobrakai

LostKobrakai

assign_new is meant to take care of inheriting assigns from a parent LV. No need to query data already available again.

jtormey

jtormey OP

Yes, but then as the developer you have to be very careful about how data coming from other processes is routed to the right components, which is what I’m trying to solve.

jtormey

jtormey OP

Could you elaborate on this? From what I can tell, you can’t actually pass assigns from a parent LiveView to a LiveView rendered with live_render/3. The only data you can pass is via the :session option, which you would then use to fetch the data needed by the LiveView, including session/user data that already existed in the parent.

Also, it’s not that I think nested LiveViews don’t have a purpose. For example, live_render/3 with sticky: true when a persistent, isolated LiveView is needed in the app (helpful for uploads that don’t interrupt on page navigation).

The advantages I believe my solution has over nested LiveViews are: it’s easier to pass data up or down the component tree (any Elixir term can be an assign), it’s less expensive (spawns a new process, but it does less, and there’s no re-fetching data), it still has the familiar LiveComponent API for receiving updated assigns from the parent (update/2).

LostKobrakai

LostKobrakai

Only half of that is true. Yes you can only pass session data around (just like with the static render → connected render). But assign_new will look up assigns like :current_user in the parent LV and only query for it if that assign doesn’t exist in the parent. So if you run your auth pipeline in the parent a nested LV will just get to use the results of it, not query all the information again.

jtormey

jtormey OP

Ah, that’s a neat feature. I didn’t know that.

Still, it’s not as explicit as assigns. And in the solution I presented, you can have a connected component deeply nested within other LiveComponents, and it will be able to receive messages from PubSub to its handle_info/3 callback. I think you lose that level of composition with live_render/3.

cmo

cmo

Name spacing the events breaks down at what point? Deeply nested components?

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 91898 914
New
byu
@chrismccord : I just saw the Extract AGENTS.md from Phoenix.new into phx.new generator commit to the phoenix project. My initial shotgu...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
AstonJ
Just a general thread to post chat/news/info relating to AI/ML stuff that may be relevant for Nx now or in the future. Got anything to sh...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
mudasobwa
While I am working on the Language Agnostic Code Audit SaaS, which uses MetaAST (spoiler: I am expecting it to be in a good shape for ann...
New

We're in Beta

About us Mission Statement