Published on Tuesday, 2020-12-08 at 12:00 UTC

Using Sessions in Phoenix LiveView

Wrapping your head around using sessions in LiveViews can be difficult, but it’s not as hard as it seems.

Original photo by Lucas Mendes on Unsplash

LiveView is an amazing piece of technology. It has made the process of building interactive web applications with Elixir so much easier. And I started using it in production for thousands of users essentially immediately after I saw Chris McCord present it at ElixirConf EU back in 2018. It’s absolutely no surprise that some concepts from LiveView are slowly making their way into other programming ecosystems.

With all its ease-of-use, there is one aspect of LiveView that can be a little difficult to wrap your head around: How do you handle user sessions in a LiveView?

What’s a Session?

A session is a way of maintaining user data across multiple page requests.

Typical use-cases for sessions include user authentication (e.g. storing the user ID of the authenticated user) or shopping carts (e.g. storing a list of product IDs the user has selected). This data is ephemeral and typically doesn’t need to be stored in your database. But you still want it to be persisted across multiple page loads.

There are two types of sessions: user-side or stateless sessions on the one hand, and server-side or stateful sessions on the other hand. Phoenix supports both types of sessions out of the box, using Plug.Sessions. For user-side sessions, all session data is signed and sent to the user as a cookie. On their next request, this signed cookie is sent back to the server and decoded. Server-side sessions only store a session ID in a cookie and store the associated data in a database or in server memory.

Sessions in Phoenix Controllers

Say, you want to implement a shopping cart as part of your application. You create a route /add_to_cart/:product_id that puts the given product in the user’s shopping cart, which is stored in a session.

Your code would look something like this:

# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  # ...

  scope "/", MyAppWeb do
    get "/add_to_cart/:product_id", CartController, :add
  end
end

# lib/my_app_web/controllers/cart_controller.ex
defmodule MyAppWeb.CartController do
  # ...

  def add(conn, %{"product_id" => product_id}) do
    cart = get_session(conn, "cart") || []

    conn
    |> put_session("cart", [product_id | cart])
    |> render("cart.html")
  end
end

In the example above, we store a list of product_ids under a session key called "cart". The session is maintained across multiple page requests, allowing the user to add multiple products to their shopping cart.

Sessions in the LiveView Life Cycle

Sessions are implemented using a Plug module. This means they operate on a %Plug.Conn{} data structure.

But if you’ve ever looked at LiveView code before, you might have noticed that conn (%Plug.Conn{}) is nowhere to be seen. Instead, LiveViews use a %Phoenix.LiveView.Socket{} (socket) struct to represent their lifecycle.

But even though we don’t have direct access to conn in our LiveView, we can read from the session in the mount/3 function.

# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  # ...
  scope "/", MyAppWeb do
    live "/cart", CartLive
  end

# lib/my_app_web/live/cart_live.ex
defmodule MyAppWeb.CartLive do
  use Phoenix.LiveView

  def mount(_params, session, socket) do
    cart = Map.get(session, "cart")
    {:ok, assign(socket, :cart, cart)}
  end
end

Like this, we can read from a previously established session. In our shopping cart example, this means we could implement a page displaying the contents of the shopping cart using LiveView. Without conn, we can’t build a route that modifies the session, though.

live_render to the Rescue!

So what if we do want to modify the session from our LiveView route?

Fortunately, it’s easy to take full control of the conn. Instead of using live/2 in our Router, we set up a regular Phoenix Controller from which we call live_render.

Let’s modify our example from above so that the add_to_cart route is rendered using the CartLive LiveView module.

# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  # ...

  scope "/", MyAppWeb do
    get "/add_to_cart/:product_id", CartController, :add
  end
end

# lib/my_app_web/controllers/cart_controller.ex
defmodule MyAppWeb.CartController do
  # ...

  def add(conn, %{"product_id" => product_id}) do
    cart = get_session(conn, "cart") || []

    conn
    |> put_session("cart", [product_id | cart])
    |> live_render(CartLive)
  end
end

As you can see, only one line has changed from the previous implementation. Instead of rendering a static template using render/2, we’re now using live_render/2.

This approach might also help you wrap your head around LiveViews in general. Unlike when using live/2 where you’re essentially invoking a hidden controller, this enforces the notion that LiveView modules are, as their name suggests, stateful views.

Modifying the Session from LiveView Events

Even if we use live_render/2 from a controller, we can only modify the session at the very beginning of our route lifecycle.

So far, we’ve been using cookie-based user-side sessions. But since LiveView operates over WebSocket, it’s not easily possible to update cookies after the initial HTTP response has been sent.

Refreshing the Session with JavaScript

You could try to circumvent this and build a solution for updating the session using LiveView hooks. The idea is that, whenever you want to update your session, you trigger an XMLHttpRequest via JavaScript. This new request allows you to once again send cookies to the user. You can find a basic implementation of this principle in an article by Henrik over at The Pug Automatic. I think it’s a very crafty approach and a good solution when you want to avoid keeping session data on your server.

Server-side Sessions

My preferred way of updating session data from LiveView events involves server-side sessions. They too are fairly easy to implement. And, leveraging the power of OTP, they come with benefits such as the ability to broadcast changes to all currently mounted LiveViews.

Check out my follow-up article on how I implemented real-time server-side sessions for LiveView with Phoenix.PubSub and how you can use them in your own projects!