LiveView: Feature to "cancel" second render

Honestly I think that it may be a tall order for people who don’t understand how LV works at a deeper level to understand the idea that not connecting is something safe or reasonable to do . The benefit of having a built in “if you don’t want to connect do this” pattern, like no_live(socket) or no_connect(socket) is that that gives you a place to explain these concepts to people, explain why you might want to do it, explain the tradeoffs, and make them feel comfortable that they aren’t somehow screwing some later thing up by using this approach.

Without that, it will always feel like something that might stop working someday, or something that might opt one out of some unknown benefit down the road.

3 Likes

To someone who doesn’t understand these concepts, I think it’s far more likely that they would assume that “dead views” are the way to do this, and would not intuit this approach. Perhaps a guide? “Rendering Liveviews without connecting the socket” or something similar?

1 Like

I am inclined to agree. On the one hand this isn’t hard to wire in from a “line of code” standpoint. And at the same time it does require touching code that, since it’s generated, people almost never look at, and aren’t always comfortable touching. This feels like a simple |> no_live helper in Phoenix that is then easy to document and see.

3 Likes

This is exactly why I suggested introducing two entirely new top level functions. Mount currently gets called twice once for dead render and once for live. Have a load function for dead render and live function for the connection load. Don’t want to connect as live view and stay dead? Don’t provide a live function. Perhaps behind the scenes that gets compiled down to the current mount with a check for if the socket is connected or not.

2 Likes

This PR introduces a new mount option called auto_connect. By default, it is true, which means that the LiveView JS will automatically connect to the server on dead render. If false, the connection will be skipped. The important difference compared to a no_live(socket) approach is that it does not suggest in any way that we should not connect when navigating from another LiveView when already connected.

8 Likes

Could it be that the problem is Controller+Template is less convenient than a single file LiveView?

And, in particular, that it could be easier to go from a static/dead page to a LiveView by changing only a few things like route definition and perhaps a top level use invocation in the controller to turn it into a LiveView? In addition to being able to colocate controller and template like in a LiveView.

I feel I can understand the wish to “default to LiveView for everything”, as I’m doing the same and writing a controller feels awkward.

But I also feel adding more modes to the LiveView lifecycle is going to make reasoning and creating a mental model of it even harder.

The original idea was to turn off the connected render. That affects existing patterns like deferring actions to the connected render, async assigns, pubsub subscriptions, etc.

Steffen proposed auto_connect: false with different semantics. IIUC that feature would create situations like a supposedly static page behaves differently if you load it via regular HTTP request or if you live navigate into it. Presumably, regular request would leave you in a disconnected state but live navigation would stay connected, changing the behavior of <.link>, for example.

So in one such LiveView’s that opted into auto_connect: false, mount would be called in either a connected or disconnected state. I think that requires a developer to understand all the nuances of the original lifecycle, plus this new mode, which is a net negative for me.

I think I’d rather revisit Controllers and see if working with them can be made more like the LiveView authoring experience.

Yes, but if the page actually uses any live features, you shouldn’t use auto_connect: false.

This came across my mind as well. Maybe what people actually want is a

defmodule MyAppWeb.DeadView do
  use Phoenix.DeadView

  def mount(params, session, conn) do
    conn
    |> assign(...)
    |> then(&{:ok, &1})
  end

  def render(assigns) do
    ~H"""
    ...
    """
  end
end

although mount might not be the correct terminology here. Then, to switch to a LiveView you “just” switch out the conn for a socket and replace use Phoenix.DeadView with use Phoenix.LiveView.

One drawback compared to auto_connect is that if you navigate to this page from a LiveView, the existing connection cannot be used and a regular request is performed instead.

1 Like

I agree.

There’s an argument in recent messages that we’re adding this explicit feature for people who don’t understand every aspect of LiveView. That it would be hard for them to figure out they can simply change app.js to connect conditionally.

If I mentalize that persona, I suspect they might have a hard time deciding what they can or cannot do in a LiveView when they use auto_connect: false.

You captured what I meant well. On the router side, we would not say live SomeModule, but instead get/post/... so that we can handle only certain HTTP verbs, or a more generic catch all.

Maybe instead of mount/3 the callback would be HTTP-verb related?

I think the important bit is the breadcrumb. People own’t understand everything, but if they search in the docs for the word “static page” and get linked to this option, the docs for this option can explain when it can and when it can’t be used etc.

But I do agree, I think that it the docs for this might deserve its own section with an explanation of the kinds of things you can’t do w/ it.

I went to look at some code I wrote some months ago that fell into this category. To my own surprise, turns out I defined two modules in a single file to be able to colocate controller and template!

I understand controllers are a bit more complicated than LiveViews in this regard because they expose more flexibility, both in targeting different HTTP verbs and rendering different types of content from the same endpoint (HTML/JSON/etc).

Here’s a gist of the code I wrote, lib/myapp_web/controllers/temp_controller.ex:

defmodule MyAppWeb.TempController do
  use MyAppWeb, :controller

  def show(conn, %{"id" => id} = _params) do
    # ...
    render(conn, :show, id: id)
  end
end

defmodule MyAppWeb.TempHTML do
  use MyAppWeb, :html

  attr :id, :integer, required: true

  def show(assigns) do
    ~H"""
    {{ @id }}
    """
  end
end

router.ex:

    # ...

    get "/temp", TempController, :show

In my understanding I wouldn’t want to change anything in the router side. I could have a single controller and choose which function to call based on HTTP verb:

    get "/temp", TempController, :show
    post "/temp", TempController, :save

On the controller, there’s a difference compared to mount/3 in that there are 2 arguments, conn and params – no session. I think that’s okay.

When I call render, IIRC the expectation is that there’s a module with a name similar to the controller TempControllerTempHTML, then I get to choose the template name which would typically be a file in disk. This part could be simplified, if I could write the template in the same module as the controller just like in LiveView.

Perhaps that could be done without changing Phoenix or LiveView, and instead updating the phx.new generator to make use MyAppWeb, :controller do some magic that “transforms a function returning HEEx in the controller module into a valid template”?

@zachdaniel does this resonate with your own needs?


I’ve also found a case where I deliberately made a static help page into a LiveView so that I can live navigate into and from it. If I would turn off the LiveSocket in this page, I would lose live navigation out of the page.

It also has the advantage that when I update the docs users immediately get to see the updated page without having to refresh.

When do you want to return JSON in a LiveView?

I believe if you were to return JSON instead of HTML with the JavaScript bits to establish a WebSocket connection, then you are in a weird place:

  • What does it mean to live navigate from a “connected, HTML LiveView” into a LiveView that returns JSON? I think this live navigation should be made impossible, because we need to send HTTP headers stating the content-type, and it cannot be done over WebSocket.
  • This “JSON LiveView” cannot support any LiveView features other than the dead render.

Why not use a regular controller + template?

That reminds me of: Controller: Implicit rendering with heex - #2 by LostKobrakai

Perfect! TIL :purple_heart:

edit: TILA (I learned again, because I had already liked that message, means I must have read it before!)

Never. We’re talking about how best to introduce live connection cancellation. My proposal is to never cancel, but be explicit about when you want to connect. Basically have a load function that works as mount before connection then live that works as mount post connection. The nice thing here is that on pages with no live function they function exactly like a single file controller, similar to a svelte file, where I can have my markup, my page load, and page updates all in one file. This is a nicer DX for regular dead pages than what we already have. It has the added benefit of allowing one to even defer the connection until later (call live/3 at some other point) making pages that are “dead for now”.

To get to your question however what I’m proposing is since we can now (in my theoretical universe) render “dead” pages like LiveViews, we can allow renders of any “dead” content like json. I just took the calling convention from LiveView native instead of ~SWIFT I chose ~JSON

This example is exactly what I want. The ergonomics of a single file for dead page renders

1 Like

I think the proposed auto_connect?: true option is exactly what I want. Because it retains the benefits of “allowing” to be connected for things like live navigation to not require a full reload, and fits very nicely w/ the current design of LV.

The auto_connect: false would be like a “deferred connected LiveView”. The LiveView may be connected or disconnected depending on how one lands on it.

How would you decide when to use that feature (vs a colocated Controller+View or a regular connected LiveView)?

For my documentation pages I guess I’d still use LiveViews in the connected fashion, so that the different behaviors are more predictable (redeploy, navigation in/out of the page, and sometimes Presence). But maybe I’m still not fully grasping the problem space.

I think I wouldn’t see a need to ever use a Controller+View if I can tell the LV not to connect. They become equivalent in ways that matter and better in other ways, right? Essentially acting as an actually static page, w/ no connect, avoids things like users seeing a “we can’t find the server” on a page that has no need of it, etc. The way that they are better is that if I push_navigate to them I don’t have to do the whole handshake again, it will just use the socket. Although I guess at that point there might be a disconnection message, but perhaps that can leverage this information too to say “don’t try to reconnect either if no LV on the page needs it” @steffend would that make sense?

I haven’t considered the redeploy scenario though. But as soon as you want things like presence, live updates etc. you’d remove the auto_connect: false setting.

2 Likes

Are things like user sessions fixed with this as well? I thought we still needed controllers for that?