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.

sodapopcan

sodapopcan

I use LiveComponents when I want opaque behaviour and state that largely doesn’t affect crucial business logic. @slouchpie’s SelectorColor is one such case I would likely use one for. Generally auto-complete dropdowns, search boxes, stuff like that. I use the function component + hooks if I want to reuse a business CRUD form (though in practice I rarely do this as this is hardly ever a need). In this case I do want the state explicitly in the mount/handle_params so I can actually see what’s going on without jumping down a rabbit hole of nodes.

Of course there are probably a lot of scenarios I’ve just never hit where this wouldn’t be so cut and dry (and not that it is cut and dry but it’s mostly worked for me).

sodapopcan

sodapopcan

LOL yes I got that fairly quickly :slight_smile: These have JS hooks but I store some state in the backend. I suppose it could be frontend too but, while I don’t dislike JS, I prefer have less of it. For example with an auto-complete that makes an API call store the current results in the LC’s state and render with HEEx. Maybe this isn’t what you’re saying, though?

Also I never thought about the library implications for co-located hooks… that is fantastic.

Where Next?

Popular in Questions Top

Fl4m3Ph03n1x
Background Let’s assume I have a typical GenServer that receives messages as requests, does some operation in a DB and returns responses....
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
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
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; someth...
New

Other popular topics Top

vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
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
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New

We're in Beta

About us Mission Statement