Prevent LiveView from mounting when using the browser back button

Hello everybody,

I’m not sure if the title is clear but here is my problem:
I have different “pages” handled within the same LiveView (so it’s a LiveNavigation).
At one moment I have a form for which I save (or update) the changeset in the socket upon every input blur.

So that when I go back and forth on the pages, when I reach back to the form, every changes is still there.
Unless if I reach the first page!

Let me explain with a simplified picture:

In the example above the form is in “page 3”, but it might be anywhere…

So once I reached the form on Page 3, I can go back and forth to Page 2 and Page 4 (going back using the browser back button - going forth, either using the browser next button or the custom live_patch next buttons - both work fine), the form changes will always be there.

However as soon as I reach back to the Page 1 (still using the browser back button), when I go back to the form everything is lost.

It appears that every time I’m reaching Page 1, the LiveView is mounted again, so all the assigns are gone.

Now, a little trick could be to use (as shown above) a custom Go Back button which will go back to the “Home” with a live navigation.
This works because in that case what’s really happening is not a come back to the previous page in the browser history, but it’s a new page in the history that is on the previous page…
But even in this scenario, if at one moment I reach back the initial Page 1, everything is gone again.

I precise that this happen even when I’m coming in Page 1 from another LiveView (I mean when the Page 1 here is not the initial browser HTTP loaded page).

So, is this the normal behavior of LiveView?
If so, how to explain that?

And what I’m interested with, is there a way to prevent that?

I tried to push_patch on mount to the page itself (using a custom handled flag in order to prevent a redirection loop), but this is not working…

A solution might be to save the changes to the database and fetch them again, but in that process the data will only be partial and not yet valid, and I don’t want to deal with partial changesets etc. And with this database approach I think I’m losing the whole purpose of why I’m using LiveView in the first place…

Thanks for taking the time…

2 Likes

Yeah we ran into stuff like this with our live view pages. Basically, the solution is to always have the full state of the form in the actual web page. You can’t use the liveview process as the canonical store for anything.

Here is another scenario that can cause issues: Suppose someone is half way through the form, and you do a deploy. Boom, all their stuff is lost.

So basically, if you want to do a multi page form like this, what I recommend is storing any form values that have been submitted on previous pages in hidden attributes on the following page. Then, should live view need to reconnect the form recovery mechanism will be able to handle all the values. Related to this you can also experiment with the replace: true option on push patch.

Finally, If you want the form state to survive no matter where you go on the site, you can also experiment with a JS Hook that stores all of the form data in local storage.

7 Likes

Hi @benwilson512 ,

Indeed I already tried this option. And to be honest, it wasn’t clear enough for me in the docs Live navigation — Phoenix LiveView v0.20.2) to what is the exact behavior with and without… I figured it out by making some tests.

This is an interesting option…
I think that this is the way I’ll go.
Thank you!
Do you have some readings (or doc links) to propose regarding this?
I think that it involves the session, right?


But just some thoughts about this solution…

In fact this can also solve another problem I encountered.
Passing assigns between LiveViews.

At first I naively thought that doing the following will end up passing the data:

# In a handle_event of one LiveView
  socket =
    socket
      |> assign(validated_email: email)
      |> push_redirect(to: "/home")

  {:noreply, socket}

# In the mount of another LiveView
def mount(_params, session, socket) do
  IO.puts(socket.assigns.validated_email) #ERROR - validated_email does not survive from previous LiveView
end

But it appears that (probably for good reasons) the assigns don’t survive from previous LiveViews…
Unless the flash!
Which I’m using to solve this problem.


For example, in my case I’m doing exactly this (a multi step form) but without even reaching to the database.
What I’m doing is everything in memory (still using changeset and using apply_changes if they are valid).
Since we have LiveView, I don’t think this is an anti-pattern.
Quite the opposite, I think that LiveView brought an interesting use-case.

If the user left the site (closing the page)…
I don’t care that when he come back all the data is lost.
This is also applicable if he hit the refresh button, or go to an arbitrary page of the process by typing the url in the browser.
Since it’s the intended usage.
In fact, currently I’m even doing this, I test for the presence of the data I’m concerned with in the flash…
If it’s not present then I push_redirect the user back to where he should have been in the beginning (ie. validating his email in this use case)


Regarding this subject, wouldn’t be a good solution, to provide some kind of “persisted” data in the context of the socket (like the assigns but persisted between LiveViews.)
Like the flash but persisted…
I mean the flash is able to be passed between LiveView but after each round-trip it’s reset.

I think that there is nothing blocking, right?

We can simply call it store!
And using the same singular/plural convention than assigns (and always returning back a socket), it can look like this…

# In a handle_event of one LiveView
  socket =
    socket
      |> store(validated_email: email)
      |> push_redirect(to: "/home")

  {:noreply, socket}

# In the mount of another LiveView
def mount(_params, session, socket) do
  IO.puts(socket.stores.validated_email) #Data available
end

We can even have some kind of API (as we have for assigns like assign and update) and maybe even add a pop that will remove a data.

Since we cannot use the session to write into, this might be a good solution.

I don’t think that this is an edge use-case of mine.

What do you think?

Hi again…

By actual web page, you mean the rendered HTML (as you said with for example hidden attributes) within the HTTP request?

I think you’re right in the common regular HTTP-based used cases…
As I said in the previous reply I guess (like for the SPA) it’s okay to want this in some cases.

Well… I never think of this kind of odds happening (maybe I should…)
But in that case I want to say that it(s not a big deal and anyway this is also applicable no matter the kind of form used. Since everything for existing user will blow up like the CSRF token etc.

Unless you are talking about data persisted in DB (and I think this is what you mean).
But again, I wanted to explore solutions that doesn’t even involve the database since until it’s validated further in the process, it’s okay to consider those data as temporary…

Hello @Sanjibukai, I’m unsure about the mount behaviour you are experiencing (since I don’t alter my path when moving between forms); but regarding persistence I currently leverage most form data to CubDB.

A couple of things to consider:

  • I have a main map/struct for the whole form to store the form state. It is validated, updated and stored in CubDB semi-regularly with each form change or submit event.
  • I have a token which is written to the url (part of the path or a querystring) and is used to retrieve the form state from the store.
  • You will probably need some security mechanism for token retrieval, to protect sensible data.
  • Also a mechanism to clean-up “forgotten” form fillups.

It has worked really well so far, but it took me some time to figure out the security aspect of tokens, the Cloak library helped a lot here.

@benwilson512 local storage recommendation sounds like a wonderful alternative.

1 Like

This solution is limited to a single node though right?

Yes it is, currently a very “small” part of the overall system so it currently sits along everything else in a monorepo, no plans for it to become distributable.

But just to confirm, lets say that I would like to move just the store to a different node… as long it is a single node consistency would not be a problem right? :thinking: Maybe just a bottleneck in the far future?

To me multi-node isn’t about bottlenecks, it’s about reliability and durability. Nodes go down, whether from issues with the underlying server or from an OOM bug or any number of reasons, and if you have multiple nodes up then your load balancer health checks can react and keep routing traffic to good nodes while your bad ones come back online or are replaced. If you only have one live node then when it goes down your system is simply down until you get it fixed.

This is why I think for this particular problem at least, the client side is the right place to store this info.

2 Likes

Right! Didn’t meant to imply it was just a bottleneck issue (which has more in common with concurrency than the number of nodes), but thanks for reminding that more nodes are really about resilience :mount_fuji:.

Definitely sounds like the simpler and right solution for transient “browser” data.

Hello @chouzar
In fact the need to change the path is about to give some hint to the user (not really needed but it’s there). Also it gives for free, browser history navigation in some way…

In some way I’m doing exactly the same, but in memory…
But do you still using Changesets?
I mean are you storing changesets data or just the bare params maps?

I tried to track the form state (which also span on multiple schemas - and this is in fact the rational for going down this route with mutlistep forms) with simply storing the form params as bare maps…

And simply use that whole map at the end and let the changesets do the work with validating.
But this works only if I don’t give feedback to the user, until the very last step…
Which is anyway unpractical, because in this case how to define in which form sent back the user to correct the inputs? The first one by default seems less than ideal.

Currently the solution I found was to encapsulate the former LiveViews as stateful LiveComponents inside a parent LiveView which has all the data in dedicated changesets in its assigns.

I’m not a fan of this solution since now I have to deal with events inside the LiveComponents and sending dedicated messages to the parent (using send on the LiveComponent and handle_info on the LiveView).
It’s not a big deal, but there’s some boilerplate to deal with.
But dealing with the forms changesets (data validation) is now dead simple…

I don’t get the security concerns related to tokens…
I mean the token will act like a key for you to retrieve in your KV store the data related to this token.
Or do you use the token itself as encrypted data?

Yes I’m also thinking that.
At the end of the day, until the user validated the form, it’s like he’s still a “public” user and there’s no need to save any data that as @chouzar said will need to be cleaned up.

Also @benwilson512 (I’m still asking :slight_smile: since you’re already dealing with this kind of stuff - I mean interaction from client side to server side)…
Do you have any resources to propose on how to interact with client-side storage within LiveView?
It would have been so easier if we could have write access to the session data.
But currently, I don’t really get how to do that.

Anyway thank you for your thoughts…

It is definitely nice to have :grin:

So maybe I’m using the wrong terminology here but I’m talking about generating some kind of string you can append at the url. Lets say that this string would be 5 character alphanumeric like:

http://your.site/info/C10FB

If that was the key of the store it would be extremely easy to generate values and look at retrieve information from the site; so my thinking is that something should be done to avoid bad agents.

I only store the main struct or map, which in my specific case is just a map.

My first approach was a very complex embedded_schema that contained other embedded_schemas and it very quickly became really hard to track with changesets. The advantage is that I had a single source for the whole form, but a couple of cases made the structure a bit annoying to manipulate with changesets.

I switched to a plain changeset per form, these changesets are exclusive for validating the specific fields of the form so componetizing it into a live component made a lot of sense :slight_smile:

When I sent the info from the component to the main liveview (only if the changeset is valid) I just message the struct (not the whole changeset) and merge with the main map:

%YourEmbeddedSchema{} 
|> from_struct() 
|> Map.merge(socket.assigns.main_form_map, fn {k,v1, v2} -> v1 end)

Not exactly like that, but the transformation is entirely dependant on the case :nerd_face: I’m still thinking on strategies to alleviate all the boilerplate that using handle_info, send and send_update involves :thinking:

Currently thinking on having a single struct standardize info sent between liveview and components, but the idea is not completely settled.

When I had a single changeset for the whole form I had to apply validations on each input to get the feedback (which in my case are just the error_helpers that the generators provide. But had some issues when I tried to evolve the schema :slight_smile:

1 Like

@chouzar @benwilson512
Meanwhile I found this upcoming stash feature described by @chrismccord here https://news.ycombinator.com/item?id=21101081 and the relevant code here https://gist.github.com/chrismccord/5d2f6e99112c9a67fedb2b8501a5bcab
So it’s all good news!