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.
Problem
When
<LiveToast.toast_group>is rendered inside a function-component layout (the modern Phoenix 1.7+ pattern whereapp.html.heexis invoked as<.app>from each LiveView template, rather than set viaput_layout), theLiveToast.LiveComponentnever mounts. Only the staticComponents.flash_groupfallback ever renders, and the streamed-toast half ofput_toast/4is 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:
This works if
app.html.heexis the route-level layout (set viaput_layout/put_root_layout), because in that case@socketand@toasts_syncare naturally in scope — they're the LiveView socket assigns the framework injects into the layout's render.It does not work when
app.html.heexis 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. Soassigns[:socket]is alwaysnilinsideapp.html.heex,connectedis alwaysfalse, andtoasts_syncis alwaysnil— regardless of whether the caller is actually a connected LiveView.This pattern is the default in
mix phx.gen.livesince Phoenix 1.7 (the<Layouts.app>style), and is whatmix phx.newproduces 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 intoapp.html.heex. Inspect the rendered DOM:#toast-groupis the static fallback (data-phx-locpoints toComponents.flash_group, notLiveComponent), missingdata-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/1and forwarded them from every caller (~20 LV templates):After that,
data-phx-componentappears on#toast-groupandput_toaststreams correctly.