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.
- 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:
-
maybe_subscribe(socket, session)
which you can call in yourmount/3
callback.
This will subscribe your LiveView process to changes in the session viaPhoenix.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 ahandle_info({:live_session_updated, session}, socket)
callback.
The function is calledmaybe_subscribe
rather thansubscribe
because it only subscribes your socket if your socket hasconnected?: true
set. -
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 aconn
struct, it allows you to store session data from a LiveViewsocket
you’ve previously subscribed viamaybe_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.