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?