slouchpie

slouchpie

Complex components lead to us always calling `detach_hook/3` before `attach_hook/4`

Greetings, comrades.

At work, we have been converting some legacy LiveComponents into functional components. This typically involves moving much of the update lifecycle function logic into a handle_params hook which is hooked using attach_hook/4.

Now we have encountered a situation that I will capture using a simple, fictional example.

Imagine a functional component SelectColorComponent that defines a helper function to attach hooks for events (from phx-click or phx-submit or similar).

defmodule SelectColorComponent do
  def select_color_component(assigns) do
    ~H"""
      <div> something something </div>
    """
  end

  def attach_hooks(socket) do
    attach_hook(socket, :select_color_handle_event_hook, :handle_event, &handle_event_hook/3)
  end

  def handle_event_hook("color-selected", params, socket) do
    # do stuff
    {:halt, socket}
  end

This works great if used directly in a LiveView. We just render the component in the HTML as usual (<SelectColorComponent.select_color_component some_assign={"something"} />) and in the mount function we make sure to call

def mount #...
   socket
  |> #...
  |> SelectColorComponent.attach_hooks()
  |> then({:ok, &1})

No problems.

However, we then write 2 new components, both of which use the SelectColorComponent.

Let’s say one is an AvatarComponent and another is a ProfileBackgroundComponent.

Both of these components render the select color component in their html and therefore have to ensure the hooks are attached

defmodule AvatarComponent do
  def attach_hooks(socket) do
    socket
    |> SelectColorComponent.attach_hooks()
   |> attach_hook(:avatar_component_handle_event_hook, # etc

and similar for ProfileBackgroundComponent.

If we use both the Avatar and Background components in our LiveView, then we need to call their attach_hooks in our mount, like so:

 def mount #...
   socket
  |> #...
  |> AvatarComponent.attach_hooks()
  |> ProfileBackgroundComponent.attach_hooks()
  |> then({:ok, &1})

This will then cause a crash (raise) because we will be calling SelectColorComponent.attach_hooks() twice.

This has led us to conclude that it is often good to simply call detach_hook before calling attach_hook since it is a no-op if the hook does not exist and it avoids raising. This way we can nest our components arbitrarily, which lets us play with them like lego.

TL;DR we have found it safest/easiest to often attach hooks like this:

def attach_hooks(socket) do
  hook_name = :select_color_handle_event_hook
  lifecycle_function = :handle_event

  socket
  |> detach_hook(hook_name, lifecycle_function)
  |> attach_hook(hook_name, lifecycle_function, &handle_event_hook/3) 
end

I am, as usual, asking any and all of you fine people to respond to this with whatever pops into your brain-tank when you see it. Is it neat? Is it hacky? Are we blind to a a more elegant approach to keep our functional components composable?

Most Liked

josevalim

josevalim

Creator of Elixir

Hi @slouchpie, we should probably have an option that allows you to replace the existing hook, so you don’t need to do this dance:

|> attach_hook(hook_name, lifecycle_function, &handle_event_hook/3, replace: true) 

Which basically says that, if it already exists, replace it. A PR is very welcome (please ping me if you do). The reason we raise is to avoid accidental overlaps but in this case it is very intentional.

I am very willing to accept that once we use more hooks, we will see some cracks in that approach too, but I really don’t think this thread is one of them. We have a safety in place, which is easy to bypass with detach_hook, and we can improve it further through pull requests.

I honestly regret writing that comment because many have read “LiveComponents being overused” as “LiveComponents being useless” while they still have many use cases. The goal was mostly to have folks discuss and evaluate alternatives.

To be clear, if you don’t have state, a function component should always be preferred over live components. When it comes to live components vs hooks though, my go-to canon is this:

  • Live components are great for sharing parts of your application (such as a form embedded in different places, as you mentioned) or encapsulating complex parts of a page. They usually interact with specific parts of your domain.

  • Hooks are great for creating abstractions that are meant to be generic and focused more on the UI.

However, it is completely fine if your team arrives to a different conclusion. The most important is what you have said before: “also motivated by discussions here with my team, we started to investigate how to refactor parts of our app”. Discuss between yourselves, try a couple of approaches, and then establish the pros and cons based on your experience in your project. And if you can share them later with the community, I am certain it will be very valuable.

garrison

garrison

You know, I really have to be honest here: I’m really starting to worry this entire pattern is a big mistake. I know there was some, uh, emotional posting going on in response to Jose suggesting this pattern a few months ago. Which is unfair, because obviously Jose has probably seen as many LiveView apps as anyone can have seen and I’m sure he only posted about it because he had seen good results with the pattern.

Personally, I realized shortly afterwards that I had no business commenting on the topic further because I had no idea what I was talking about, and I’ve spent the last couple of months studying various frontend framework implementations and reading through old articles and posts to see why certain decisions were made, particularly around React hooks.

And what I’ve noticed, frankly, is that they had the same problems. The React community went through nearly exactly the same set of problems with components, and state, and composability, last decade, and through a lot of trial and error (and mistakes, and cleverness) eventually came up with React function components and hooks as a solution.

The problem is that most components, whether you like it or not, are not properly expressed as pure functions. Real components often need to allocate state, and the allocation of that state makes the component idempotent rather than pure.

If you are ideological about purity, you can cheat by putting the state allocation in the mount of a parent component (perhaps the root LiveView) and then “pretend” that you now have pure functions. But now you have a new problem: this approach does not properly compose.

In React-land, back in the day, people started to solve this problem by allocating stateful components to handle business logic because the components actually composed properly. But this makes things messy, because you end up with components in the tree that aren’t actually components. We could do similar ugly things with LiveComponents if we wanted to, although incidentally LiveComponents do not compose properly either because they have global ids, which is another problem.

Anyway, there were a number of ideas for how to make these things compose properly. Here is a wonderful article from the time detailing many of them. The solution they landed on, quite (in)famously, was to rely on call order to allocate hooks in a resumable way.

So, getting to the point: the mistake here is that we are allocating our hooks (and our state, via assign) with flat keys. For state, this composes mostly fine as long as you use components (global ids notwithstanding). But once you try to compose things which are not components, like those LiveView hooks, you are running into collisions. Because using a flat key structure doesn’t compose.

One solution would be to do what React did and rely on call order. Another potentially workable solution from that article is to namespace the allocations somehow, perhaps automatically. Unlike JS, we have macros (and total control of the language), so there are probably things we can get away with that they could not.

But what I’m really trying to drill home here, fundamentally, is: this is not a new problem. So we would do well to look at how others have solved it in the past.

garrison

garrison

Oh yeah, there are definitely some that have server state, and those have to be LiveComponents. But a lot of “control” type components only have client-side state, like maybe a date picker or something.

Autocomplete is a good example of server-side state, especially if the list of things you’re completing comes from the LiveView. On the other hand, if it’s a short list you might be better off rendering them all into the HTML and then selectively showing/hiding with JS. That sort of thing would be more elegant with a proper JS framework (i.e. React), but for simple stuff imperative vanilla JS can get the job done, especially if most of your app’s complexity is actually server-side and handled (declaratively) by LiveView!

Some sort of magic integration between server- and client-side declarative libraries would be the holy grail. I know there are many working on this from different angles, e.g. LiveSvelte/Vue/React, Hologram, etc. Personally I plan to roll a client library from scratch and see where that gets me.

Last Post!

garrison

garrison

BTW, I don’t think you should regret the post. It seems to me that it was quite successful, seeing as we’re discussing alternatives right now! :slight_smile:

I am curious what others think of the idea of finding a way to add optional state to otherwise pure functions. Another advantage of React’s hook approach (as I mentioned somewhere above) is that they got rid of the dichotomy of “class/function” components, which seems to be a problem we’re dealing with here.

I think the biggest argument against would be that, since Elixir functions are (generally) actually pure, we would be violating that mental model by mounting state within them. On the other hand, that’s essentially what LiveComponents are doing, and we get by, so I’m not sure.

Where Next?

Popular in Questions Top

nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

Other popular topics Top

nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 44532 311
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New

We're in Beta

About us Mission Statement