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?

First 10 of 18 Posts! Switch mode

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.

endoooo

endoooo

is this the post you’re talking about?

not only because of this comment from Jose, but also motivated by discussions here with my team, we started to investigate how to refactor parts of our app moving away from LiveComponents (which, in our case, I do think are overused) towards function components + hooks. I’m using a lot of the insightful suggestions we can find in this forum, but I must confess I’m having a hard time to find a design that feels “right”, for multiple reasons (e.g. these collisions caused by “global” flat keys in assigns you mentioned).

right now, I’m somewhat confused about what are good scenarios for LiveComponent vs function components. as a not so seasoned developer like you guys (and as someone coming from a non-technical background), I think more practical examples/guides would be very helpful. but I understand that, for this specific topic, this is something that may be going through some changes in terms of best practices; e.g. the new Phoenix 1.8 generators does not use LiveComponents anymore — altough it came with some changes in the UX, dropping the modal form (what if I wanted to reuse the form in different views, would LiveComponents be a good option, or is there a way to implement it using function components?)

thank you for sharing how you’re trying to solve those problems and for bringing up such important discussions!

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).

garrison

garrison

That was the post I was referring to, yes. There was another discussion more recently where Jose mentioned it again; I don’t remember where but you could probably find it by going through his posts :slight_smile:

I’m not sure I’m going to give you a satisfying answer (someone else will probably post something more helpful), but what I intended with my reply above was to convey that I think there is something fundamentally wrong with how components and other component-like things are composed in LiveView. Before (in that thread you linked, for one) I thought this could be papered over with some sort of reactive “memo” mechanism. I had also posted about this in one of the Ash threads and Zach had replied with a very nice example of how it could be implemented as a simple wrapper around assign(), without changing the engine.

However as I have learned more (like I said I really had no business commenting on the topic tbh) I’m not so sure there is an easy fix. I think the correct solution would be to substantially refactor or redesign the LV engine with something that looks a lot more like React+Hooks - where components are mounted based on their implicit position in the tree (like React), and state/effects can be properly composed (like React hooks).

Now obviously that would be a pretty serious thing to go around proposing, and I would probably want to spend a lot of time working through a proof of concept, etc before I would even consider arguing for something like that. But that’s how I’ve been feeling about things lately.

Also: like I mentioned I don’t think the design would have to be exactly like React’s components/hooks. I never liked the call order thing either, although I have come to accept that they were probably right that it was the best solution for them. I really feel like there’s some way we could make automatic namespacing work, but I’m not entirely sure what the API would look like. This is definitely something that would require a lot of exploratory programming to figure out!

garrison

garrison

I use LiveComponents to break up large LiveViews. I am perhaps somewhat unique on here in that I have been experimenting with highly interactive LiveViews, i.e. things that in the past many probably thought LV can’t do. But it can, actually, because the rendering model is declarative like React’s. There are just some thorns preventing components from composing properly; in particular, that they are mounted with global ids.

A lot of my LiveComponents are singletons within the LiveView, but they’re split off because they are > 1000 lines of functionality and I don’t want a 10,000+ line LiveView because my brain isn’t big enough to work on that :slight_smile:

Another thing I use LCs for is to optimize diffs over the wire. LV does not properly diff collections (i.e. for comprehensions in your template) because there is no key to work with. However it looks like a fix for this might land in 1.1, so this point will finally no longer be relevant. Which is wonderful progress!

I do this too but I’ve noticed that a lot of these are actually happier as hooks with the state stored client-side, because they are often actually client-side functionality. I think the new co-located hooks are going to make this a lot easier to ship in a library, which is fantastic.

Edit: I meant JS hooks in the above paragraph. I’m starting to think we may have overloaded the word “hook” a little too much lol

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.

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.

Schultzer

Schultzer

I have never found a good use case for LiveComponent that a function component cannot do better, and lead to an improved design. If you can remove abstractions, do it.

FlyingNoodle

FlyingNoodle

Im curious about this take. Would you mind addressing the issues of OP, then?

I’m genuinely curious what you would suggest about how to do what OP is asking.

garrison

garrison

Function components have no lifecycle; they are unable to mount/unmount state and side effects. LiveComponents can be mounted declaratively in the template, whereas if you want to mount state or effects for a function component you have to do it imperatively in an event handler somewhere. At scale it gets very messy.

Furthermore, as demonstrated here in the OP, state (assigns) and effects (here, lifecycle hooks) cannot be composed because they have flat keys, so it is impossible to abstract anything out.

It’s worth pointing out that React Hooks did something funny: instead of maintaining two separate classes of component (Class Components and Function Components), one stateful and one stateless (pure), they instead came up with a way for function components to optionally register state.

That’s very clever because it side-steps the need to train developers on which type of component to use. You just use function components every time, and if you need state, you call a hook. If you don’t, the function is pure! Perhaps going down that path would help new devs learn LiveView, seeing as overuse of LiveComponents does seem to be a big problem.

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?

Trending in Questions Top

jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
silverdr
Using Phoenix.LiveView.TagEngine as an EEx.Engine is deprecated! To compile HEEx, use Phoenix.LiveView.TagEngine.compile/2 instead. Sta...
New
saveman71
Hello ! We want new/edit form pages to POST/PUT to their own URL rather than the resources REST defaults (post /things, put /things/:id)...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
bottlenecked
Hi all, I wanted to ask how the community is dealing with post-release steps. Today we have Ecto migrations, which make sure that the db...
New
michallepicki
I am using Oban and occasionally, shortly after a deployment, a handful of jobs can fail because of dependency on other parts of the syst...
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
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
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
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New

We're in Beta

About us Mission Statement