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

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
Phillipp
Hey, I have a NanoPi-M3 and try to install Elixir on their Ubuntu image. I followed the Raspberry Pi installation instructions from the ...
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
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
Exadra37
Sometimes I want to check if the input into a function is not a blank string. My first approach: defmodule Example do def do_stuff(s...
New

Other popular topics Top

yurko
Here are few pieces of (common) Linux knowledge that we use for reasonably small one server apps. We use Ubuntu but this should work for ...
New
peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
Tee
can someone please explain to me how Enum.reduce works with maps
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
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
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
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
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

We're in Beta

About us Mission Statement