Mount vs handle_params on the LiveView life cycle

Hi all,

I’m a bit confused about the role of handle_params in the LiveView life cycle. I’ve read https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#module-handle_params-3 many times and I still don’t understand it, hopefully someone can clarify.

Let me lay out what I understand from the docs:

  1. mount is called twice, once on the initial HTTP request and again on the websocket connect. (BTW the docs read “once per LiveView life-cycle” which is quite confusing).
  2. handle_params is called after mount, meaning it’s also called twice.
  3. Both mount and handle_params take the same arguments and trigger a render.

The docs suggest we should load data in mount as it’s called once and mention handle_params is used to handle live_patch operations showing the following example:

def handle_params(params, _uri, socket) do
  socket =
    case params["sort_by"] do
      sort_by when sort_by in ~w(name company) -> assign(socket, sort_by: sort)
      _ -> socket
    end

  {:noreply, load_users(socket)}
end

load_users(socket) is not listed but it implies it’s a function that load the users from somewhere and sorts it using sort_by. Assuming we’re also loading data in mount wouldn’t that mean the loading is happening twice?

If handle_params is called regardless, takes the same arguments as mount, and is invoked on live_patch whereas mount is not, why not always load the content there instead of in mount?

Are we supposed to avoid loading if some elements are already present in the socket.assigns from mount?

It may be that I’m misunderstanding something but I’ve been confused about this for a while.

Thanks in advance!

6 Likes

I’m just starting out with LiveView, but I think to me it would be cleaner to have separate callbacks for connected and disconnected mount, something like static (init?) na connected instead of just mount. As you wrote, handle_params adds even more to the mix, but that’s related to navigation, which I haven’t used so far.

You would load it on mount OR in handle_params.

The golden rule is: always load data on mount. But if you add live navigation with live_patch, the data that can be changed on-the-fly must be now loaded on handle_params.

29 Likes

Hey José,

Thanks for your reply, that makes sense.

Do you have any advice on which of the two lifecycles the loading should occur? As in the initial HTTP connection versus the websocket connection?

I’m having trouble rendering “static” HTML that has meaningful content but avoiding loading the content twice. Feels like the first HTTP connection should render a simple “loading” markup and branch on socket.connected? and perform the loading then.

If you want SEO and/or a working website without javascript you’d need to load stuff for both renders. The HTTP request and the websocket connection are completely separate connections (by time as well as type) so you cannot simply keep state around on the server. If those points are not relevant you can just load stuff for the websockets connection and make the static render return just a loading screen.

5 Likes

Hi @LostKobrakai, that makes a lot of sense, thanks for the clear explanation.

Fair points! I think this could be mentioned explicitly in the docs (unless I missed it). I might be mistaken, but it seems people are using LiveView mostly for web apps where SEO doesn’t matter.

@josevalim writes about a simple technique to load everything once here.

1 Like

It surely depends on the content. For many web-applications even a non-interactive static response can be useful in the face of e.g. some failures for the js/websocket stuff. If we’re talking e.g. about games though the static render alone doesn’t hold much usefulness.

Not sure if that’s one of the intended usages of LiveView, but if it were to be used as a replacement for SPAs, then the static render isn’t really useful, except maybe for a loading spinner. (BTW now that I think of it, the socket assigns can grow a lot if not enough care is taken.)

I’m of the opposite opinion. Often times SPAs are used because the page needs e.g. live update or lazy loading of a list of items. If things start to fail on the client side (for whatever reason) you’re often left with nothing. Many of them could show a lot of useful information if they would have a static render or even fall back to e.g. plain old pagination links if the fancy js lazy loading does fail.

4 Likes

All SPA frameworks offer server-side rendering precisely to have meaningful content on first page view. Not only for SEO but also, as @LostKobrakai mentions, to allow graceful degradation into a partially functional application.

I guess it depends on the type of app you’re building, but I think most apps should try to return content on first page render.

How can you define different things to render in the first and on the second rendering? I though we could only use one template (assuming you have a live route) and as such you would need to render the same content.
Could you point some example/info, please?

Missed this link in the thread…it’s explained here: Shortcomings in LiveView

I have a page that loads content based on the first URL parameter and uses live_patch in its navigational event handlers—If there is no parameter, then it provides a default.

Knowing that handle_params always gets called and I don’t care about checking any mount-type behaviour (like socket_connected?) should this golden rule still apply? I found I was able to simplify my code (and still have it work with JS disabled) with the following:

  @default_assigns [
    entity: DB.find("default_entity"),
    # ... more keyword list entries
  ]

  @impl true
  def handle_params(%{"term" => term}, _session, socket), do:
    {:noreply, assign(socket, @default_assigns |> Keyword.replace!(:entity, DB.find(term)))}
  def handle_params(_params, _session, socket), do:
    {:noreply, assign(socket, @assigns)}

  @impl true
  def handle_event("submit", %{"term" => entity}, socket), do:
    {:noreply, push_patch(socket, to: "/#{entity}", replace: true)}

The site in question is an art project and this works so there is nothing at stake here, I’m just trying to grok best practices.

Note: DB is just an example and not something that makes a database call (even Ecto isn’t involved). I’m pretty sure I can understand why setting attribute values based on a db lookup is probably a bad idea… but is it??

Ok, thanks!

I would say your snippet is correct. Since that particular parameter/assign can change, it should be loaded in mount instead of handle_params.

Thank you for the response!

Did you mean my snippet is incorrect?

My thing is that I want to handle loading / and /:entity, so to handle it in mount, I’d need my two implementations of mount to handle them (def mount(%{"entity" => entity}, ...) and def mount(_params, ...)) and then that exact same handling logic would be repeated in equivalent handle_params implementations. Since handle_params gets calls after mount anyway, why should I bother assigning assigns in mount in this case?

Thanks again!

Your snippet is correct. :slight_smile: the params that can change are loaded on handle_params.

2 Likes

Thank you so much for your response. I realize I’m being somewhat pedantic though I’m quite new to functional programming (under a year) and want to ensure I understand the paradigms and avoid shoehorning my OO and Rails knowledge into Elixir and Phoenix.

Hoping someone can check my work on this issue, since I haven’t had the opportunity to work with LV in production much:

I ran into an issue where I had some data that needed to be loaded in different ways depending on the content of the params, and if it was not loaded correctly, it would fail. I had moved all the logic into handle_params already, but because on initial load the query params were not passed, and since the absence of the query param is a valid option in some cases, I couldn’t rely on pattern matching to always skip the logic in question as I have in other cases in the past. As a result certain views would break because it would try to execute the code as if the param were absent even when it was actually present in the uri.

def handle_params(params = %{"path_param" => needs_y, "x" => y}, _, socket) do
  socket = assign(socket, :data, load_func(needs_y, y))
  {:noreply, socket}
end

def handle_params(params = %{"path_param" => no_y_needed}, _, socket) do
  socket = assign(socket, :data, load_func(no_y_needed) # <- this fails on initial render before "x" is passed
  {:noreply, socket}
end

One option I considered was actually parsing the URI directly to determine whether param was present or not and wait until it was passed if so:

def handle_params(params = %{"path_param" => maybe_needs_y}, uri, socket) do
  if String.contains?(uri, "x") do
    if x = params["x"], do: assign(socket, :data, load_func(maybe_needs_y, x), else: {:noreply, socket} #wait
  else
    assign(socket, :data, load_func(maybe_needs_y)
  end
end

…but that seemed awkward and potentially fragile.

So instead what I did was introduce a loading param that I set to the value of connected/? in the mount function.

def mount(_, _, socket) do
  # other stuff
  socket = assign(socket, :loading, !connected?(socket))
  {:ok, socket}
end

def handle_params(_, _, %{loading: true}), do: {:noreply, socket} # wait

def handle_params(params, _, socket) do
  # query params present, exec loading data
end

So far this seems to be working but my sense is that this may not be what the connected? check is intended to be used for.