Don't want to miss out on Elixir news? Subscribe to ElixirPulse!

How do I access conn assigns in the LiveView socket?

Here’s the short answer: You can’t.

The sort of short answer: Store the minimum amount of information necessary in the session so that the connected callback of the LiveView can get all of the data it needs to function.

The slightly long answer: Use assign_new/3 to transparently read from the conn’s cache in socket.private.assign_new during disconnected render. Pass a function to assign_new/3 that relies on the session so the connected render can retrieve the data it needs. If one reads no further, this should be the takeaway.

The full answer requires a deeper dive into LiveView’s life cycle, the conn, and the socket.

Demystifying LiveView’s Lifecycle

One of the most common questions asked by people getting their feet wet with LiveView is some variation of: “How do I transfer assigns from the conn to the socket?” See:

It seems so simple: There’s an assign available in the conn.assigns and yet, when trying to access it in the LiveView, it’s unavailable. Why? It’s made even more confusing by the fact that the LiveView docs specifically state: “It is possible to share assigns between the Plug pipeline and LiveView”

The key is that the full quote states: “It is possible to share assigns between the Plug pipeline and LiveView on disconnected render

What’s a disconnected render? LiveView has two types of mount/render cycles:

The first is the initial page load which takes place over HTTP (sometimes referred to as the “dead” render).

The second happens when the WebSocket connection to the server is created.

Due to the stateless nature of HTTP, The WebSocket connection has no way to obtain information from the HTTP request (and, by extension, the conn struct). This is why it’s impossible to pass the assigns from the conn to the assigns field of the socket. It can be counter intuitive to people coming from more traditional SPA backgrounds that the mount and render callbacks are invoked more than once on a page load.

What now?

None of the above helps answer the question “How do I get assigns from the conn to the socket?”

There are two ways:

  1. Use assign_new/3 during disconnected render
  2. Store data in the session

On a disconnected render, the conn passes its assigns to the socket.private.assign_new map. This allows assign_new/3 to access them on initial load. When the connected render begins, enough data should be stored in the session so that the connected mount can retrieve the data it needs.

Here’s a fairly typical example: Assigning the currently logged in user to the conn in a plug pipeline:

If your application uses phx.gen.auth the user_token is stored in the session and the current_user is stored in the conn.assigns (code omitted for brevity):

# user_auth.ex
conn
|> put_session(:user_token, token)

# user_auth.ex
assign(conn, :current_user, user)

The LiveView mount/3 callback then looks like:

def mount(params, session, socket) do
  socket =
    socket
    |> assign_new(:current_user, fn -> Accounts.get_user_by_session_token(user_token) end)

  {:ok, socket}
end

On disconnected render assign_new/3 will look in the socket.private.assign_new map for the key current_user and assign the value found to the socket’s assigns.

On the connected render, because the conn is not available to the WebSocket, the function is invoked to retrieve the current user based on the user_id stored in the session.

This is why assign_new/3 accepts a function as its third argument. If it wasn’t a function, it would be evaluated even if the assign was available in the assign_new private field and would make an unnecessary database call.

How do we know this is true? Check out the Phoenix.LiveView.Static module. Phoenix.LiveView.Static “Holds the logic for static rendering”. AKA, the disconnected render.

On line 105 we can see that the conn.assigns is passed as part of the second argument of the function Phoenix.LiveView.Utils, which sets the socket’s private field.

socket =
  Utils.configure_socket(
    %Socket{endpoint: endpoint, view: view, router: router},
      %{
        assign_new: {conn.assigns, []},

This is how LiveView uses assign_new/3 to share assigns from the conn “cache” to the socket.

A note about sessions

When storing data in the session it’s important to remember to store just enough to retrieve what is necessary for rendering. Typically this is some basic information about a user - maybe a user ID or account ID.

One might be tempted to save a database call on every page load and store entire structs in the session. Avoid the temptation - users of the application will soon be hit with 431 errors. The linked page states a 431 error can happen if:

  • There are too many Cookies sent in the request

Which is exactly what will happen if entire structs are stored in the session. In addition, the session now functions as a sort of cache which is guaranteed to make things all kinds of go haywire.

Conclusion

In the end, it’s sort of possible, but not really, to access data from the conn in the socket. This is by design. Jose Valim states:

The cache is transparent because you are not supposed to rely on connection assigns

TL;DR:

Use assign_new/3 to transparently read from socket.private.assign_new during disconnected render. Pass assign_new/3 a function that relies on session data so the connected render can retrieve the data it needs.