Skip to content

Conversation

hugo-a-b
Copy link
Contributor

As noted in the documentation for Phoenix.LiveView.attach_hook/4:

Hooks attached to the :handle_event stage are able to reply to client events by returning {:halt, reply, socket}.

However, doing so from within a LiveComponent, where :handle_event hooks are supported, resulted in an error (see below for a minimal app reproducing the bug):

** (ArgumentError) invalid return from PhoenixPlayground.Router.DelegateLive.handle_event/3 callback.

Expected one of:

    {:noreply, %Socket{}}
    {:reply, map, %Socket{}}

Got: {:halt, %{"hello" => "world"}, #Phoenix.LiveView.Socket<...>}

Looking through the source of the functions in the stack trace, I noticed that Phoenix.LiveView.Channel.inner_component_handle_event/4 was missing a case for {:halt, reply, socket} return values from hooks. By adding the missing case, the bug now appears to be resolved.

Reproducible example

Mix.install([
  {:phoenix_playground, "~> 0.1.8"},
  {:phoenix_live_view, "~> 1.1.13"}
])

defmodule DemoComponent do
  use Phoenix.Component

  def display(assigns) do
    ~H"""
    <div id="my-client-hook" phx-hook="ClientHook">DemoComponent</div>

    <script>
    window.hooks.ClientHook = {
      mounted() {
        this.pushEventTo(this.el, "ClientHook:mounted", {hello: "world"}, (reply) => {
          console.log("received reply:", reply)
        });
      }
    };
    </script>
    """
  end
end

defmodule DemoLiveComponent do
  use Phoenix.LiveComponent

  def render(assigns) do
    ~H"""
    <div>
      <h2>DemoLiveComponent</h2>
      <DemoComponent.display />
    </div>
    """
  end

  def mount(socket) do
    socket =
      attach_hook(socket, :reply_on_client_hook_mounted, :handle_event, fn
        "ClientHook:mounted", params, socket ->
          {:halt, params, socket}
  
        _, _, socket ->
          {:cont, socket}
      end)
  
    {:ok, socket}
  end
end

defmodule DemoLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  def render(assigns) do
    ~H"""
    <h1>DemoLive</h1>
    <.live_component module={DemoLiveComponent} id="demo-live-component" />
    """
  end
end

PhoenixPlayground.start(live: DemoLive, port: 9001)

As noted in the documentation for `Phoenix.LiveView.attach_hook/4`:

> Hooks attached to the `:handle_event` stage are able to reply to
> client events by returning `{:halt, reply, socket}`.

However, doing so from within a LiveComponent, where `:handle_event`
hooks are supported, resulted in an error:

```
** (ArgumentError) invalid return from PhoenixPlayground.Router.DelegateLive.handle_event/3 callback.

Expected one of:

    {:noreply, %Socket{}}
    {:reply, map, %Socket{}}

Got: {:halt, %{"hello" => "world"}, #Phoenix.LiveView.Socket<...>}
```

Looking through the source of the functions in the stack trace, I
noticed that `Phoenix.LiveView.Channel.inner_component_handle_event/4`
was missing a case for `{:halt, reply, socket}` return values from
hooks. By adding the missing case, the bug now appears to be resolved.
@SteffenDE SteffenDE merged commit e2b3c62 into phoenixframework:main Oct 6, 2025
6 of 7 checks passed
@SteffenDE
Copy link
Collaborator

Thank you! 🙌🏻

SteffenDE pushed a commit that referenced this pull request Oct 7, 2025
As noted in the documentation for `Phoenix.LiveView.attach_hook/4`:

> Hooks attached to the `:handle_event` stage are able to reply to
> client events by returning `{:halt, reply, socket}`.

However, doing so from within a LiveComponent, where `:handle_event`
hooks are supported, resulted in an error:

```
** (ArgumentError) invalid return from PhoenixPlayground.Router.DelegateLive.handle_event/3 callback.

Expected one of:

    {:noreply, %Socket{}}
    {:reply, map, %Socket{}}

Got: {:halt, %{"hello" => "world"}, #Phoenix.LiveView.Socket<...>}
```

Looking through the source of the functions in the stack trace, I
noticed that `Phoenix.LiveView.Channel.inner_component_handle_event/4`
was missing a case for `{:halt, reply, socket}` return values from
hooks. By adding the missing case, the bug now appears to be resolved.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants