Navigating using patch causes liveview to re-mount when using browser back button

I’m trying to find out if this is just fundamentally how it works due to some limitations I’m unaware of, or if this behavior can somehow be changed. I’m hoping someone might be able to shed some light on this before I spend more time going down a rabbit-hole.

Consider this simple LiveView:

defmodule MyAppWeb.NavigationLive do
  use MyAppWeb, :live_view

  def render(assigns) do
    ~H"""
    <p>Hello! You have navigated <%= @count %> times.</p>

    <div class="flex gap-4">
      <.link patch={~p"/navigation?page=#{@page - 1}"}>&lt;- previous page</.link>
      <.link patch={~p"/navigation?page=#{@page + 1}"}>next page -&gt</.link>
    </div>
    """
  end

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

  def handle_params(params, _url, socket) do
    {:noreply,
     assign(socket,
       page: (params["page"] || "1") |> String.to_integer(),
       count: (socket.assigns[:count] || -1) + 1
     )}
  end
end
  1. When you click a link that causes a patch, only handle_params gets called, as expected.
  2. Click a link again, only handle_params gets called, as expected
  3. Use the browser back button, only handle_params gets called, as expected
  4. Use the browser back button again, it causes the live view to re-mount and lose its state

This feels “wrong” to me, but again, I might just be missing something. I feel like after any one patch, I should be able to hit back and it only invoke handle_params again without needing to re-mount. Is this possible?

Odd, you’re right! I’ve been sitting here trying to reason about this but it feels like a bug to me. I would open an issue.

Thanks for confirming that it also feels odd to you too, I’ll do a little more digging and depending on what I find I might open an issue or maybe even a PR to change it :sweat_smile:

Hey @doughsay, did you find anything else regarding this?

I looked through the issues and pull requests for LiveView but couldn’t see that anything matching has been filed.

:folded_hands:t2:

EDIT: Found this also.

https://forum.elixirforum.com/t/prevent-liveview-from-mounting-when-using-the-browser-back-button/34601

I ran into this a little while ago and was planning on making a thread about it, but I guess you guys beat me to it! It caused me some problems while writing hooks and I definitely agree the behavior is bugged and should be fixed.

I did a little poking around and I think I have an idea what’s going on. When a patch is executed, pushState() is called on the client with a state that looks like {id: '...', type: 'patch'} where the id is the id of the current LiveView. Then, when the user navigates back/forward, the popstate event is triggered with that state. The LiveView JS then checks whether the current id matches the id in the state to determine whether to patch or replace the LiveView, presumably to handle a user navigating between LiveViews.

So the first case of the bug is obvious: when the user loads the initial page, there is no pushState to store the id, and so when the user navigates back, the check fails (the state from the event is undefined) and triggers a remount.

However, whoever writes the fix for this needs to be careful, because there is another case to handle: when the user loads the LiveView, then navigates, then reloads, then navigates, the browser stores the previous pushState state from before the reload as the state for the reloaded page. Because the reload remounts the LiveView, the id which lives on in the browser stack is different from the current LiveView id, causing the same buggy behavior as above.

I’m not sure what the best fix for this would be. One solution might be to call replaceState() with an initial state on the first mount. Perhaps @chrismccord can offer his thoughts :slight_smile:

https://github.com/phoenixframework/phoenix_live_view/pull/3335

Ah, that will do it then :slight_smile: Thanks!