Skip to content

LiveToast.LiveComponent never mounts when toast_group is rendered inside a function-component layout #59

@Flo0807

Description

@Flo0807

Disclaimer: I investigated this with Claude Code, so the analysis and language below come out of that pairing.

Problem

When <LiveToast.toast_group> is rendered inside a function-component layout (the modern Phoenix 1.7+ pattern where app.html.heex is invoked as <.app> from each LiveView template, rather than set via put_layout), the LiveToast.LiveComponent never mounts. Only the static Components.flash_group fallback ever renders, and the streamed-toast half of put_toast/4 is silently dropped.

User-visible symptom: LiveToast.put_toast(socket, :info, "...") only produces a single Phoenix flash; nothing ever renders via the LiveComponent stream, and stacked / dismissible toasts don't work. There's no error, no warning — the integration just silently degrades to flash-only behaviour.

Why the README's wiring doesn't catch this

The README recommends:

<LiveToast.toast_group
  flash={@flash}
  connected={assigns[:socket] != nil}
  toasts_sync={assigns[:toasts_sync]}
/>

This works if app.html.heex is the route-level layout (set via put_layout / put_root_layout), because in that case @socket and @toasts_sync are naturally in scope — they're the LiveView socket assigns the framework injects into the layout's render.

It does not work when app.html.heex is a Phoenix function component (def app(assigns)) invoked from each LiveView template. Function components only receive the attrs explicitly passed at the call site; they don't auto-inherit the caller's assigns map. So assigns[:socket] is always nil inside app.html.heex, connected is always false, and toasts_sync is always nil — regardless of whether the caller is actually a connected LiveView.

This pattern is the default in mix phx.gen.live since Phoenix 1.7 (the <Layouts.app> style), and is what mix phx.new produces today. So new projects that follow modern generators and then follow the LiveToast README end up with a silently broken integration.

Reproduction

A minimal repro is a mix phx.new-style project where the LV templates call <Layouts.app> as a function component and drop the README's <LiveToast.toast_group ...> snippet into app.html.heex. Inspect the rendered DOM: #toast-group is the static fallback (data-phx-loc points to Components.flash_group, not LiveComponent), missing data-phx-component. LiveToast.put_toast(socket, :info, "test") from any LV then produces only a flash, never a streamed toast.

What I had to do to make it work

In my project I added the missing attrs to my Layouts.app/1 and forwarded them from every caller (~20 LV templates):

# layouts.ex
attr :socket, :any, default: nil
attr :toasts_sync, :list, default: nil

def app(assigns)
<!-- every LV template -->
<Layouts.app
  flash={@flash}
  ...
  socket={@socket}
  toasts_sync={assigns[:toasts_sync]}
>

After that, data-phx-component appears on #toast-group and put_toast streams correctly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions