How to pass flash messages between 2 LiveViews living in 2 different live_sessions

I want to pass a flash message from a LiveView to another LiveView, but those 2 LiveViews doesn’t share the same live_session (in the router), both uses the same pipeline and that pipeline calls :fetch_live_flash.

The call to Phoenix.Flash.get(@flash, :info) returns nil

If I merge both live_sessions, it works as expected, but I need these LiveViews to be in 2 separate live_sessions (because they use different root_layout, and different on_mount hooks)

Is it an intended behaviour or a bug ?

Edit: here’s a minimal reproducing this behaviour

I can’t answer definitively but I would not expect the flash to sustain between live sessions as they’re, well, new sessions. From what I know the flash is attached to the socket, which is dropped, then a fresh socket is booted.

So you probably need some other storage like ETS (with a stale cleaner) or your actual DB and passing a token between the redirect.

At first I thought you could just grab the flash in your on_mount hook but that will render the flash when disconnected, then immediately clear it on the connected render (which might be considered un-intuitive if not a bug maybe).

Not a great idea
diff --git a/lib/flash_redirect/application.ex b/lib/flash_redirect/application.ex
index e28840b..9b77e2c 100644
--- a/lib/flash_redirect/application.ex
+++ b/lib/flash_redirect/application.ex
@@ -13,9 +13,10 @@ defmodule FlashRedirect.Application do
       # Start the PubSub system
       {Phoenix.PubSub, name: FlashRedirect.PubSub},
       # Start the Endpoint (http/https)
-      FlashRedirectWeb.Endpoint
+      FlashRedirectWeb.Endpoint,
       # Start a worker by calling: FlashRedirect.Worker.start_link(arg)
       # {FlashRedirect.Worker, arg}
+      %{id: Agent, start: {Agent, :start_link, [fn -> %{} end, [name: Example]]}}
     ]
 
     # See https://hexdocs.pm/elixir/Supervisor.html
diff --git a/lib/flash_redirect_web/live/a_live.ex b/lib/flash_redirect_web/live/a_live.ex
index 9726664..a261ba1 100644
--- a/lib/flash_redirect_web/live/a_live.ex
+++ b/lib/flash_redirect_web/live/a_live.ex
@@ -8,10 +8,10 @@ defmodule FlashRedirectWeb.ALive do
 
   @impl true
   def handle_event("redirect", _, socket) do
-    socket =
-      socket
-      |> put_flash(:info, "Hey!")
-      |> push_navigate(to: "/b")
+    key = :crypto.strong_rand_bytes(8) |> Base.url_encode64()
+    :ok = Agent.update(Example, &Map.put(&1, key, "hey!"))
+
+    socket = push_navigate(socket, to: "/b?token=#{key}")
 
     {:noreply, socket}
   end
diff --git a/lib/flash_redirect_web/router.ex b/lib/flash_redirect_web/router.ex
index 499c057..e4e5da1 100644
--- a/lib/flash_redirect_web/router.ex
+++ b/lib/flash_redirect_web/router.ex
@@ -1,4 +1,16 @@
 defmodule FlashRedirectWeb.Router do
+  defmodule Hack do
+    def on_mount(:default, %{"token" => token}, _session, socket) do
+      flash = Agent.get_and_update(Example, &pop_in(&1, [token]))
+      socket = Phoenix.LiveView.put_flash(socket, :info, flash)
+      {:cont, socket}
+    end
+
+    def on_mount(:default, _params, _session, socket) do
+      {:cont, socket}
+    end
+  end
+
   use FlashRedirectWeb, :router
 
   pipeline :browser do
@@ -31,7 +43,7 @@ defmodule FlashRedirectWeb.Router do
     end
   end
 
-  live_session :b do
+  live_session :b, on_mount: FlashRedirectWeb.Router.Hack do
     scope "/", FlashRedirectWeb do
       pipe_through :browser

(Dont actually use an Agent for this).

Since it “flashes” the flash, you might be better off pulling the message in the actual live view mount/handle_params instead and then you can be sure to only do it after connection via connected?.

Not sure if there is a better way.

So, the Live Navigation docs (Live navigation — Phoenix LiveView v0.20.2) include this key information:

The “navigate” operations must be used when you want to dismount the current LiveView and mount a new one. You can only “navigate” between LiveViews in the same session.

You’re currently using push_navigate/2, but if you swap it out for redirect/2, you’ll find that the flash message correctly appears on the page.

4 Likes

Works perfectly with a redirect/2 !
Thank you :wink:

I was curious why this works,

Gumshoes

If we look at push_navigate and redirect they both attach a redirected command tuple to the socket:

  def push_navigate(%Socket{} = socket, opts) do
    opts = push_opts!(opts, "push_navigate/2")
    put_redirect(socket, {:live, :redirect, opts})
  end

phoenix_live_view/phoenix_live_view.ex at ce0986071a1c469e580cb97808168a27109e659f · phoenixframework/phoenix_live_view · GitHub

  def redirect(%Socket{} = socket, to: url) do
    validate_local_url!(url, "redirect/2")
    put_redirect(socket, {:redirect, %{to: url}})
  end

phoenix_live_view/phoenix_live_view.ex at ce0986071a1c469e580cb97808168a27109e659f · phoenixframework/phoenix_live_view · GitHub

via put_redirect

 defp put_redirect(%Socket{redirected: nil} = socket, command) do
    %Socket{socket | redirected: command}
  end

phoenix_live_view/phoenix_live_view.ex at ce0986071a1c469e580cb97808168a27109e659f · phoenixframework/phoenix_live_view · GitHub

I think this should then pass into handle_changed in channel.ex

  defp handle_changed(state, %Socket{} = new_socket, ref, pending_live_patch \\ nil) do
    new_state = %{state | socket: new_socket}


    case maybe_diff(new_state, false) do
      {:diff, diff, new_state} ->
        {:noreply,
         new_state
         |> push_live_patch(pending_live_patch)
         |> push_diff(diff, ref)}


      result ->
        handle_redirect(new_state, result, Utils.changed_flash(new_socket), ref)
    end
  end

phoenix_live_view/channel.ex at ce0986071a1c469e580cb97808168a27109e659f · phoenixframework/phoenix_live_view · GitHub

where maybe_diff does a small check if there is a redirected command:

  defp maybe_diff(%{socket: socket} = state, force?) do
    socket.redirected || render_diff(state, socket, force?)
  end

phoenix_live_view/channel.ex at ce0986071a1c469e580cb97808168a27109e659f · phoenixframework/phoenix_live_view · GitHub

So we should kick down to handle_redirect with the command. In both cases we copy the flash and then call push_redirect or push_live_redirect

      {:redirect, %{to: _to} = opts} ->
        opts = copy_flash(new_state, flash, opts)


        new_state
        |> push_redirect(opts, ref)
        |> stop_shutdown_redirect(:redirect, opts)


      {:live, :redirect, %{to: _to} = opts} ->
        opts = copy_flash(new_state, flash, opts)


        new_state
        |> push_live_redirect(opts, ref, pending_diff_ack)
        |> stop_shutdown_redirect(:live_redirect, opts)

phoenix_live_view/channel.ex at ce0986071a1c469e580cb97808168a27109e659f · phoenixframework/phoenix_live_view · GitHub

Push redirect and push_live_redirect kick out to the JS client with a different key:

 defp push_redirect(state, opts, nil = _ref) do
    push(state, "redirect", opts)
  end


  defp push_redirect(state, opts, ref) do
    reply(state, ref, :ok, %{redirect: opts})
  end


  defp push_live_redirect(state, opts, nil = _ref, {_diff, ack_ref}) do
    reply(state, ack_ref, :ok, %{live_redirect: opts})
  end


  defp push_live_redirect(state, opts, nil = _ref, _pending_diff_ack) do
    push(state, "live_redirect", opts)
  end

phoenix_live_view/channel.ex at ce0986071a1c469e580cb97808168a27109e659f · phoenixframework/phoenix_live_view · GitHub

If we look around the JS, the redirect handlers are very similar, but…

When opening the console we can see an unauthorized live_redirect. Falling back to page request… And if we look in session.ex…

Since we cross the session boundary, we get forcibly dropped as we’d expect (and want!) but this means the “fall back to page request” is a new one without the flash copied over, vs redirect where we intentionally attach some flash data and reload the whole page, which may “cross a session boundary” but since it’s not navigation inside the session, it doesn’t hit the same check and the flash is retained :slight_smile:

4 Likes