Published on Tuesday, 2020-12-15 at 12:15 UTC

Introducing Phoenix LiveSessions

Building better Phoenix Sessions with LiveView integration and PubSub support

PhoenixLiveSession is a drop-in replacement for the default Plug.Session stores, allows modifying session data from LiveViews, and broadcasts changed sessions via Phoenix.PubSub.

I’ve previously written about how to use sessions in Phoenix LiveViews. The short of it is: It’s easy to store data in the session when a user first loads a LiveView page, but you need hacky JavaScript to modify the session when a user interacts with the LiveView.

This was not an answer I was content with, so I decided to dig deeper.

Plug.Session

Sessions in Phoenix are implemented using Plug.Session. And like most parts of Plug, Plug.Session has been designed with modularity in mind.

When you generate a new Phoenix app using mix phx.gen, your endpoint.ex is going to include a bit like this:

@session_options [
  store: :cookie,
  key: "session",
  signing_salt: "my-salt"
]

This is the configuration telling Plug.Session to use Plug.Session.COOKIE for storing all session data. Plug.Session.COOKIE is an implementation of the Plug.Session.Store behaviour and one of two stores that ship with Plug by default.

While Plug.Session.COOKIE, as the name implies, stores the entire session in a cookie, Plug.Session.ETS stores the session in an ETS table. The cookie then only contains the session ID.

But even though Plug.Session.ETS stores session data in-memory on the server, it has no interface for modifying said data outside of a regular Plug pipeline.

Fortunately, we can simply create our own modified version of Plug.Session.ETS and allow the modification of sessions outside of Plug pipelines. And while we’re at it, why not make it possible to subscribe to session changes via Phoenix.PubSub?

Why Do You Want This?

There are many instances in which it would be convenient to store user session data from a LiveView.

My go-to example is an e-commerce site with a shopping cart feature. Users need to be able to add products to the shopping cart, remove them, modify their quantities, and so on. LiveView would be perfect for this, making it easy very easy to handle dynamic user interactions.

But once you’d start building your shopping cart in LiveView, you’d realize that this isn’t so easy after all. In order to persist the changes a user wants to make to their shopping cart, you would have to create a special API constantly triggered via JavaScript applying those changes to the session or even to a database. And if the user has the shopping cart page open in another tab as well, you would quickly end up with an inconsistent state.

This is why it would be convenient to be able to modify sessions from LiveViews. No hacky JavaScript required, and session data is automatically shared with other LiveViews and regular Phoenix controllers.

Introducing PhoenixLiveSession

So I decided to build PhoenixLiveSession, a backend for Plug.Session. You can check it out on GitHub and Hex.

PhoenixLiveSession ...

  • is a drop-in replacement for the default Plug.Session stores
  • allows modifying session data from LiveViews
  • broadcasts changed sessions via Phoenix.PubSub
  • and takes care of removing expired sessions from memory.

Setup

Setting up PhoenixLiveSession is very straightforward.

  1. Add it to your mix.exs file as a dependency:
    {:phoenix_live_session, "~> 0.1"}
  2. Configure your endpoint to use PhoenixLiveSession as its session store:
     # lib/my_app_web/endpoint.ex
       @session_options [
           store: PhoenixLiveSession,
           pub_sub: MyApp.PubSub,
           signing_salt: "your-salt"
       ]
    

Usage

In regular Phoenix controllers, you don’t need to change anything. Since PhoenixLiveSession is a full implementation of the Plug.Session.Store behaviour, everything will continue to work as before.

The interesting bit happens in LiveViews.

PhoenixLiveSession introduces two new functions you can use in LiveViews:

  1. maybe_subscribe(socket, session) which you can call in your mount/3 callback.
    This will subscribe your LiveView process to changes in the session via Phoenix.PubSub. Whenever a change to the session is made, whether from another LiveView process or a regular controller, you get notified and can take care of the updated session in a handle_info({:live_session_updated, session}, socket) callback.
    The function is called maybe_subscribe rather than subscribe because it only subscribes your socket if your socket has connected?: true set.

  2. put_session(socket, key, value) which you can call from anywhere in your LiveView.
    put_session works just like its cousin does in Plug pipelines. The only difference is that, instead of operating on a conn struct, it allows you to store session data from a LiveView socket you’ve previously subscribed via maybe_subscribe.

Here is an example of how to use the two functions in practice:

defmodule ShoppingCartLive
  use MyAppWeb, :live_view

  def mount(_params, session, socket) do
    socket = socket
    |> PhoenixLiveSession.maybe_subscribe(session)
    |> put_session_assigns(session)

    {:ok, socket}
  end

  def handle_info({:live_session_updated, session}, socket) do
    {:noreply, put_session_assigns(socket, session)}
  end

  def handle_event("add_to_cart", %{"product_id" => product_id}, socket) do
    updated_cart = [product_id | socket.assigns.cart]
    PhoenixLiveSession.put_session(socket, "cart", updated_cart)

    {:noreply, socket}
  end

  defp put_sesion_assigns(socket, session) do
    socket
    |> assign(:shopping_cart, Map.get(session, "shopping_cart", []))
  end
end

Who is this for?

Since PhoenixLiveSession uses an ETS table, session data will be lost when you restart your server. ETS also doesn’t support multi-node setups. If you want to persist your sessions even across server restarts or use LiveSessions in a multi-node environment, it would be quite easy to modify the code to store the data in Mnesia or another database instead. But for most projects on single-server setups, ETS should be sufficient.

Contributions Welcome!

If you have ideas for improving PhoenixLiveSession, please feel free to open an issue on GitHub or create a Pull Request. Also, check out the full documentation on Hexdocs.