Why weren't LiveComponents implemented as processes?

Apologies if this is a stupid question but I couldn’t find any existing answers to it from Google/this forum.

The problem that my team’s encountering is that our LiveViews end up becoming so bloated from all of their message handling they have to do on behalf of child components. A big place where this comes up is PubSub. We have so many handle_info clauses in our LiveViews because we can’t offload them to the child components. This impacts our ability to create well encapsulated components.

Does anyone have any insight into this?

Or, suggestions for how to create smaller, simpler, LiveViews?

6 Likes

The answer here is a bit tautological: LiveComponents exist to rather explicitly not be processes, because processes have (some) overhead. If you want processes you can create more LiveViews.

Indeed, the lack of a unified messaging system is unfortunate. But you can work around it by building your own. I’ve been doing something like this, which works well:

# Main LiveView
def handle_info({:event, event}, socket) do
  dispatch_event(event, socket)
  {:noreply, socket}
end

defp dispatch_event({:folder, _status, _data} = event, _socket) do
  send_update Components.Sidebar, id: "sidebar", event: event
end

In reality I put one more level of indirection between the pattern matching functions and the send_update() calls, but it’s the same thing. Note that the dispatch here is static - I added the socket parameter in case I wanted to dispatch dynamically (allowing a component to register itself) but as my app has grown more and more complex I have yet to find any use for that functionality.

# Child LiveComponent
def mount(socket) do
  # ...
end

def update(%{event: {:folder, :created, %{id: id}}, socket) do
  folder = Folders.get!(id)
  {:ok, add_folder(socket, folder)}
end
# ... and so on

def update(assigns, socket) do
  # the normal update/2 callback
  {:ok, assign(socket, assigns)}
end

So the components receive their events and act accordingly. I’ve been pretty happy with this pattern overall - I do have some issues with the PubSub-for-realtime paradigm, but the real issue is upstream (Postgres is the problem) so it’s not really relevant here.

7 Likes

Completely agree with @garrison and a small thing I wanted to add is that nobody stops you from creating a new process for each of your live component, it’s fairly trivial to do that and there were cases when I did that.

2 Likes

Instead of live components you can also consider using attach_hook (+ on_mount) to manage certain pieces of state in a composable manner and rending those with function components. With hooks you can stay within the parent LVs boundaries (have access to handle_info) while keeping code separate.

6 Likes

Your example would have been helpful in the documentation for Phoenix.LiveComponent — Phoenix LiveView v1.0.11 when I was looking for this last week. Do you post these tips anywhere?

1 Like

To answer your question on “why” - it’s because it has been designed and implemented like that. Jose gave his opinion about LiveComponents in this thread.

I think the proposed solution to break down big LiveViews is to use attach_hook/4 in a way that you’re able to register in the partent liveview handlers coming from the Components. But you have to do it explicitly, which assumes you even know what components will be rendered down the road, and this breaks the whole idea of encapsulation UI and code and business logic into components.

When LiveView was originally announced, I remember the authors talking that it will never be something that allows you to replace things like React, and instead it’s a way to “sprinkle” bits of interactivity on top of otherwise static web sites. But I think we’re using it now to build full-featured apps, effectively replacing front-end frameworks with LiveViews and are running into design decisions that were made assuming we just want to “sprinkle interactivity” on static pages.

3 Likes

Chris did later update that opinion around the time he had that drag-and-drop demo on display.

Are you talking about deeply nested or dynamic components? I like the attach_hook method but the components I use that for are generally either top level or there is only one of them on the page.

I have updated that comment once more because it seems it is still misunderstood. However, even back when I first wrote it, the first paragraph said (emphasis mine):

Today we have function components, LV hooks, JS commands, and others which can be better suited for creating abstractions.

It seems the whole context of my reply, which is about generic abstractions, and the mention to alternative approaches have been discarded to push a narrative I did not intend and already clarified more than once.

The push away from LiveComponents is nothing new either. If we go back to before Phoenix v1.7, everyone was using LiveComponents to implement modals, and then the Phoenix team showed they could be implemented with just functions components + JS commands. I still think LiveComponents can work well when breaking a large LiveView apart, for optimizing payloads, etc.

5 Likes

Ouch. For what it’s worth, I have followed that discussion and I did understand your intent / disclaimers.

My reservations on that topic stem from something else entirely, namely that we still need de facto go-to approaches for some of these things with LV, and those are still in flux. But component libraries et.al. started solving those problems gradually with time.

Yes, deeply nested components. LV needs to know that somewhere down the tree of components that will be rendered, a handle_info hook will be required.

For example, I have a “WeightInput” component that reads data from electronic scale. It needs to send a message to scale, and will receive handle_info over PubSub. The solution is to attach these handlers on the LiveView, and not on the LIveComponent, but it requires the top level LiveView to know that a WeightInput component may appear on the screen.

In that particular case I have a list of Shipments, I can “Ship” them from the list, and for some of them a WeightInput will show up in a form rendered in a modal.

We have lists of shipments in multiple places and different LiveViews. If a client will want to have “Ship” button / modal on some other list, a developer implementing the feature will have to not only render that component/button on the page but also make sure he or she is handling the PubSub messages.

I mean, the current approach works. It’s just not self-contained in a LiveComponent, as it can’t be.

3 Likes

I hope I did not misrepresent it now, but yes, I linked to the post so that OP can read him/herself. I did only mention attach_hook, yes, should have mentioned others like JS commands, LV hooks and function components, but focused on handle_info case the OP mentioned.

And also I do think that attach_hook method is better than just repeating the same code over and over in LiveViews.

1 Like

One way I like to do to “encapsulate” components is like this (untested pseudo-code):

defmodule MyComponent do
  def le_component(assigns) do
    ~H"""
    <div>amazing html</div>
     """
  end

  def handle_task_complete(socket) do
    socket |> put_flash("all done!") |> then(&{:noreply, &1{)
  end

  defmacro __using__(_opts) do
    quote do
      alias unquote(__MODULE__)
      def handle_info(:task_complete, socket), do:
        unquote(__MODULE__).handle_task_complete(socket)
    end
  end
end
defmodule MyLiveView do
   use MyComponent

  def render(assigns) do
    ~H"""
    <MyComponent.le_component />

I will do this a lot For example, an accounts settings page with multiple forms - I think it is nice if each form has its own module.

Note that use can make compilation warnings and errors a bit hard to troubleshoot because stacktrace will point to the use line in the LiveView module instead of the problem in whatever module has the __using__.

3 Likes

At that point you can also use on_mount MyComponent and let it attach_hook the :handle_info, which wouldn’t come with the downsides of injecting callbacks with macros.

9 Likes

Correct, you beat me to it. It allows you to co-locate the definitions of handle_info functions that the Component will need to be installed on parent LiveView in side the Component module itself and is indeed better solution than macro.

1 Like

Not currently. I do have a presently-neglected blog on which I will probably write more about LiveView once my code is production-ready and I can publicly link to it :slight_smile:

This forum is very helpful for such tips though. I have picked up many similar patterns from others on here!

3 Likes

Hey wait a minute, is it possible to attach a hook in a LiveComponent? The docs don’t mention it so I assume not, but that would potentially be a pretty nice solution to the dispatch problems mentioned above.

One really nice thing I appreciate about the async APIs in LiveView is that they work transparently on LiveComponents. I’m sure that was an annoying bit of extra work, but the APIs are really good. Particularly the care taken to ensure later invocations of start_async properly clobber earlier ones.

3 Likes

It’s the other way around. You attach hooks from Component into LiveView.

1 Like

This is a better approach. My technique was devised before the introduction of attach_hook. I need to break my old-school habits!

1 Like

How do you do that? I can’t find any documentation about this TBH, the only thing I found is this part |> attach_hook(:sort, :handle_event, &MySortComponent.hooked_event/3) which is not the same thing since the attach_hook still needs to be called explicitly and directly in the LV, not inside the component.

1 Like

You understand this correctly. It needs to be called directly and explicitly in the LV.

1 Like