jeremyjh

jeremyjh

LiveView and Rolling Restarts

One thing I’ve wondered about since LiveView is announced is how well it can work in an environment where continuous deployment and rolling restarts are used. At work we don’t have fully automated continuous deployment but in practice we usually deploy as soon as changes are merged to master and a new container is built, often several times a day. In a larger team I could easily see many deployments per day. These events do not impact users today at all, whether you are using an SPA or a traditional server-rendered application.

In LiveView, if a user is in the middle of editing a form when new code deploys, they are going to lose all the contents of the form they’ve entered by default as their current connection disconnects and they connect to a new server which calls mount again. We could go to heroic lengths to prevent such tragedies via server-side strategies including hot reloading or stashing the state somewhere, but that introduces significant new complexity to the app. Also, at least as far as forms are concerned, it is unnecessary since we already have a copy of the state we care about (what the user has typed in) in the client.

Does anyone know if there any plans on the horizon to stash/restore form input state on disconnect/reconnect events on the client side? I expect at some point there will be a lifecycle API so that you could manually do this yourself with client-side code. But it seems like the requirements of basic form state would be pretty common across applications.

Marked As Solved

chrismccord

chrismccord

Creator of Phoenix

Duping what I posted on the phoenix issue for posterity:

Form recovery is supported with js hooks, but is not yet automatic. Automatic recovery is on the roadmap, but we will have to have the client check to ensure all its stashed inputs match the latest rendered state from the server. So in some cases for multi-step or heavily dynamic forms, auto recovery won’t work, but for the basic cases we can have the client automatically recover. In the meantime and for advanced forms, you can annotate your form with phx-hook="SavedForm", then define a JS hook which stashes the form state and passes it back up via connect params, for example:

# <%= f = form_for @changeset, "#", phx_hook: "SavedForm", phx_change: :validate, phx_submit: :save %>

def mount(_session, socket) do
  changeset =
    case get_connect_params(socket) do
      %{"stashed_form" => encoded} ->
        %User{}
        |> Accounts.change_user(Plug.Conn.Query.decode(encoded)["user"])
        |> Map.put(:action, :insert)

      _ ->
        Accounts.change_user(%User{})
    end

  {:ok, assign(socket, changeset: changeset)}
end
let serializeForm = (form) => {
  let formData = new FormData(form)
  let params = new URLSearchParams()
  for(let [key, val] of formData.entries()){ params.append(key, val) }

  return params.toString()
}

let Params = {
  data: {},
  set(namespace, key, val){
    if(!this.data[namespace]){ this.data[namespace] = {}}
    this.data[namespace][key] = val
  },
  get(namespace){ return this.data[namespace] || {} }
}

Hooks.SavedForm = {
  mounted(){
    this.el.addEventListener("input", e => {
      Params.set(this.viewName, "stashed_form", serializeForm(this.el))
    })
  }
}

let socket = new LiveSocket("/live", {hooks: Hooks, params: (view) => Params.get(view)})
socket.connect()

Note that the above url encodes the form, which requires decoding it on the server. You could write more JS to serialize the form as json to avoid this step :slight_smile:

11
Post #8

Also Liked

jeremyjh

jeremyjh

I think it does not replace input contents when it receives updates in a reply message, but it does something different on reconnect/mount. But yes maybe that is just a bug, that would be great if it can simply be solved at that level. I can recreate it using the User edit page in the example repo. I did do a mix deps.update phoenix_live_view to confirm its happening on master.

I think if forms are handled in a general way, other kinds of wierdness could probably be handled by either using query params or maybe some custom client code using life-cycle hooks (I understand they plan to implement those). If the client has a way to capture arbitrary state at the disconnect event, it could send a message at reconnect to help restore that state for example.

Another option would be to fallback to HTML controllers on disconnect. This would even work in the form example; you’d lose your live validations but it would be nice if you could prevent a disconnect from disabling the UI. Then all you have to do is implement a controller for the PUT/POST route. It already works that way if you disable Javascript and implement that one controller method, but on disconnect you get to watch the spinning ball without recourse.

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Is this true? I got the impression that morphdom was configured to not eliminate form contents. Maybe you’re running into a bug for this specific thing?

However, the general point stands. If there are items on the screen that you hold in the live view state, a deploy will eliminate that state and the page will look weird. Some of this can be handled by pushing state into the params, but it’d be nice if there was maybe a local storage API? If that worked across pages then it’d be rather complicated though.

jeremyjh

jeremyjh

Yes its not very scalable unless you introduce a dedicated backing store and if you think about the issues involved in doing it reliably I think you’ll realize its harder than supporting hot upgrades, because you still have to solve the state versioning/migration issue. This is why I think doing as much on the client as possible is the better course.

Where Next?

Popular in Discussions Top

AstonJ
Are there any Elixir or Erlang libraries that help with this? I’ve been thinking how streaming services like twitch have exploded recentl...
New
rms.mrcs
A couple of days ago I was discussing with a friend about different approaches to write microservices. He said that if he was going to w...
New
pdgonzalez872
If this has been asked here before, please point me to where it was asked as I didn’t find it when I searched the forum. Maybe a mailing ...
New
nburkley
AWS re:Invent is on at the moment with some interesting announcements. One new feature in particular is the Lambda Runtime API for AWS La...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
RudManusachi
What configs will make sense to put to runtime.exs? – A bit of how I configure apps: I have generic configs in config/config.exs, dev...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New

Other popular topics Top

minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New

We're in Beta

About us Mission Statement