TZWZ's personal page
How to allow LiveComponents to put flashes on the page
10.04.2025
Elixir
Phoenix
Phoenix LiveView

If you program using Phoenix and LiveViews you eventually have the sad realization that Phoenix.LiveView.put_flash(...) doesn't have any effect when used from LiveComponent. We can bypass this limitation by realizing that LiveViews are all just elixir processes, GenServers and functions. Because of that we can send a message to root LiveView and then update socket.assigns in it. We can do that almost transparently, as LiveViews have a thing called server side hooks. Using hooks we can add it once and have that functionality available in all LiveViews.

To start, let's create a new module for our new function that will deal with passing flashes where they should end up in:

defmodule MyAppWeb.Flash do
  alias Phoenix.LiveView

  def put_flash!(socket, kind, msg) do
    send(socket.root_pid, {:put_flash, kind, msg})
    socket
  end
end

This will send a message even when called directly in root LiveView. LiveComponent has the same pid as root_pid, so there's no real way to figure out if put_flash! got called from LiveComponent or LiveView. It shouldn't ever be much of a problem realistically, as flashes are mostly put after human interaction, which are pretty slow from a computer perspective.

Now we need a hook that will listen to these messages and update socket's assigns:

defmodule MyAppWeb.LiveHook.PutFlash do
  use MyAppWeb, :live_view

  def on_mount(:default, _params, _session, socket) do
    {:cont, attach_hook(socket, :put_flash, :handle_info, &handle_info/2)}
  end

  def handle_info({:put_flash, kind, msg}, socket) do
    {:halt, put_flash(socket, kind, msg)}
  end

  def handle_info(_msg, socket), do: {:cont, socket}
end

Then we have to attach that hook to LiveViews. To do that, we have to add it in MyAppWeb.Router when we define live route:

live_session :put_flash_live_session,
    on_mount: [MyAppWeb.LiveHook.PutFlash] do
  # Routes...
end

After that, for simplicity, we may as well import put_flash!(...) in LiveViews and LiveComponents. We have to edit macros in MyAppWeb edit macro for :live_component and :live_view:

defmacro live_component do
  quote do
    # ...
    import MyAppWeb.Flash, only: [put_flash!: 3]
  end
end

defmacro live_view do
  quote do
    # ...
    import MyAppWeb.Flash, only: [put_flash!: 3]
  end
end

Reducing amount of messages sent

If, for some reason, we want/need to avoid sending messages from root LiveView to itself when not needed, we can just use Phoenix.LiveView.put_flash(...) directly in LiveViews. But that only works if we know whether they are/will be nested at some point. But maybe we don't know that, or we want one function. To deal with this case, we can change the Flash module's function to something like this:

# For LiveComponents
def put_flash!(kind, msg) do
  send(self(), {:put_flash, kind, msg})
end

# For root LiveView
def put_flash!(socket, kind, msg) when socket.root_pid == self() do
  LiveView.put_flash(socket, kind, msg)
end

# For nested LiveViews
def put_flash!(socket, kind, msg) do
  send(socket.root_pid, {:put_flash, kind, msg})
  socket
end

And modify MyAppWeb's macros accordingly.

With this, we lose one function signature for everything, but no message is sent when using put_flash! directly in root LiveView. Also, LiveComponent doesn't have to (well, it can't) pass socket anymore.

This is a bit over the top, as sending these messages shouldn't really be a problem. Only real benefit may be if we really don't want to pass socket to put_flash! in LiveComponents if it would simplify application code.

Related projects