LiveView: Merge assigns from conn to socket

Currently when I assign a value in conn.assigns in the router, the assigned value is not merged with the socket.assigns.

Patching this line https://github.com/phoenixframework/phoenix_live_view/blob/v0.11.1/lib/phoenix_live_view/static.ex#L114 to include the assigns from conn works.

However, I do not understand if there is any reason for not doing this. Also I see the conn.assigns is copied to private.assign_new but don’t understand the purpose.

Anyone having background on this could explain the rationale?

7 Likes

Live view renders happen twice. There is the first static render that happens as part of an HTTP GET request. The conn is evaluated, a socket built / mounted, the page rendered. After that though the socket goes away entirely, it was just there to address the HTTP get request, there is no persistent connection.

Then the javascript on the page runs, connects via websocket, and does a second render. This time the socket sticks around because you’ve got a persistent connection. Notably though on this run, there is no @conn. In fact it’s a totally separate request, that could even happen to a totally different server from the GET request.

In the GET request, the assigns can be copied into the socket, because the socket is just basically exists to help you avoid writing totally different code for the static render. BUT on the second request, there’s no @conn for the values to be copied from at all. This is why the socket |> assign_new function takes an anonymous function arg. On the GET request assign_new will just use the assign from the @conn. But on the second live request, how can it get its value? It runs the anonymous function.

13 Likes

Thanks @benwilson512 for the explanation on the socket life cycle. That was useful.

Does this mean, I cannot have a pipeline plug in my router that can assign a value and have it available in all my live view? basically this is what I am after.

In normal page requests, I can set for eg user_id in the session and load the user, assign it in the conn in my plug in the router pipeline. Setting it on the router level, helps not to do the same operation in multiple controllers.

However, in the case of LiveView, I can only set the session value in the pipeline plug and have it available in all LiveView pages but have to load assigns individually for every liveview individually. is there a solution for this?

2 Likes

This is basically correct. What I did in my app is create a %Context{} struct which holds things like the current user. Then I setup this context for both plug and in my live view:

In my plug:

    case Common.Sensetra.lookup_context(conn.host, org_id, token) do
      {:error, :no_grant} ->
        conn
        |> redirect(to: "/")
        |> halt

      {:ok, ctx} ->
        conn
        |> assign(:context, ctx)
        |> assign(:host, conn.host)
        |> merge_assigns(assigns_from_context(ctx))

      {:error, %{"errors" => _}} ->
        conn
        |> put_session(:token, nil)
        |> redirect(to: "/")
        |> halt()
    end

In my live view:

  # elsewhere
  import CommonWeb.Socket
  # then
  def mount(params, session, socket) do
    socket = init_assigns(params, session, socket)
defmodule CommonWeb.Socket do
  import Phoenix.LiveView

  @doc """
  This initializes the socket assigns
  """
  def init_assigns(params, session, %Phoenix.LiveView.Socket{} = socket) do
    %{
      "token" => token
    } = session

    socket
    |> assign_new(:token, fn -> token end)
    |> assign_new(:host, fn ->
      get_connect_params(socket)["host"] || raise "pass in the host!"
    end)
    |> setup_context(session, params)
  end

  defp setup_context(socket, _session, params) do
    socket =
      socket
      |> assign_new(:context, fn ->
        {:ok, ctx} =
          Common.Sensetra.lookup_context(
            socket.assigns.host,
            params["organization_id"],
            socket.assigns.token
          )

        ctx
      end)

    socket
    |> assign(CommonWeb.Plug.Application.assigns_from_context(socket.assigns.context))
  end
end

I use assign_new because I also call Common.Sensetra.lookup_context() inside of a plug. That plug will do things like redirect you to login if there is no current user. Since I look it up in the conn, I use assign_new so that in the socket part of the GET request it isn’t called twice.

You can mostly ignore the whole "host" thing I pass in. Our application does subdomain based theming so it’s important for us.

12 Likes

Thanks @benwilson512 for these code examples. Much appreciated. If I am not mistaken, basically the following code is what is required to set assigns to the socket that are common and it has to be manually wired in every LiveView where the common assigns is required.

  In my live view:
  import CommonWeb.Socket
  
  def mount(params, session, socket) do
    socket = init_assigns(params, session, socket)

Do you think it calls for a feature like in plug macro in Phoenix Controllers?

@josevalim @chrismccord is it a good feature request to LiveView? Basically, as of now, there exists no way to set a common assigns shared by different LiveView pages like by setting them in pipeline plug (as in the case of normal http requests)

Could we have a LiveView Pipeline that takes in a socket and return an updated socket before it reaches the individual LiveView mount? Do you think this is an important addition? If so, I am interested to contribute.

Ideas that comes to me is a plug-like interface that takes in a socket and returns an updated socket.

In the MyApp’s endpoint.ex, we could have an additional preprocess key like:


  socket "/live", Phoenix.LiveView.Socket, 
    websocket: [connect_info: [
        session: @session_options,
        preprocess: MyApp.LiveViewPlug
      ]]

and we could use this config here: https://github.com/phoenixframework/phoenix_live_view/blob/master/lib/phoenix_live_view/socket.ex#L51 to load assigns in all LiveView.

Or we could have Phoenix.Controller.Pipeline style plug so that each liveview can use a preprocessors to @socket like

plug GetCurrentUser

If there is a better approach to do this, let me know. As mentioned earlier, I am looking for a cleaner way to set assigns that are common across multiple LiveView pages.

4 Likes

Yeah I’ve thought about this too. It’s definitely sub optimal to need to remember to put that at the top of every live view I have. At the same time it’s also nice and explicit, and low magic.

5 Likes

In your example, where is the %Context{} struct saved?
In an ETS table, in a database?

It isn’t saved, it is constructed on each request from the conn assigns / session.

3 Likes

The documentation for assign_new/3 at:

https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#assign_new/3

says: “When a LiveView is mounted in a disconnected state, the Plug.Conn assigns will be available for reference via assign_new/3, allowing assigns to be shared for the initial HTTP request.”

Example only shows passing stuff through session though. How does one “reference Plug.Conn assigns” in such case?

You don’t do it explicitly. The cache is transparent because you are not supposed to rely on connection assigns. You need to assume you need to get everything from scratch.

I might be a bit slow today… but… how do I get everything from scratch? Do I have to put everything I need into session, or?

You want the minimal information you need on the session for fetching all the information you need for the LV.
E.g. you need the user, then put the user_id into the session, so you can load the full user based on the id.

Right, that part is clear. What bakes my noodles here is the sentence from the docs: “… the Plug.Conn assigns will be available for reference via assign_new/3, allowing assigns to be shared…”. How is that done?

If you’re in the static render and you do assign_new(socket, :user, fn -> fetch_user() end) then phoenix will look at conn.assigns.user first and copy that value before falling back to executing the anonymous function.

6 Likes

Gosh! Now I see that the example actually makes sense in the context, TNX (yet another time)!

3 Likes

If you think the docs can be clearer, a PR is welcome!

Heh - I put down a TODO yesterday about it! But since you so nicely invited… :wink: Yet, it seems the current master branch has this part already updated (and better). I still slightly modified it though, addressing even more explicitly the part that had me baffled yesterday. PR is out for grabs.