This is the Elixir library for building servers for LiveState applications.
The goal of LiveState is to make highly interactive web applications easier to build. Currently, in most such applications, clients send requests and receive responses from and to a server API. This essentially results in two applications, with state being managed in both in an ad hoc way.
LiveState uses a different approach. Clients dispatch events, which are sent to the server to be handled, and receive updates from the server any time application state changes. This allows state to have a single source of truth, and greatly reduces client code complexity. It also works equally well for applications where updates to state can occur independently from a user initiated, client side event (think "real time" applications such as chat, etc).
LiveState shares similar goals to LiveView, but takes a different approach which allows for building different kinds of applications. LiveView allows the user to write all of the application code, both server logic and view presentation logic, in Elixir, and entirely manages the web client side of the application. LiveState event handlers are written in Elixir and are quite similar to LiveView event handlers, but LiveState relies on client code to render state and dispatch events. This trade-off keeps client side code simple, but allows LiveState to be used to build applications that are not as good of a fit for LiveView.
This package can be installed
by adding live_state
to your list of dependencies in mix.exs
:
def deps do
[
{:live_state, "~> 0.8.1"},
{:cors_plug, "~> 3.0"}
]
end
While cors_plug
is not strictly required, you will very likely want it to be able to add to your endpoint so that
clients cannot connect to your channel.
First you need to set up a socket as you would with other normal Phoenix Channels
- On your Endpoint module, set up a socket for your channel:
defmodule MyAppWeb.Endpoint do
socket "/socket", PgLiveHeroWeb.Channels.LiveStateSocket
...
- Then create the socket module with the topic to listen to:
defmodule MyAppWeb.Socket do
use Phoenix.Socket
channel "topic", MyAppWeb.Channel
@impl true
def connect(_params, socket), do: {:ok, socket}
@impl true
def id(_), do: "random_id"
end
- Create your channel using the
LiveState.Channel
behaviour:
defmodule MyAppWeb.Channel do
use LiveState.Channel, web_module: MyAppWeb
...
- Then define your initial state using the
c:LiveState.Channel.init/3
callback, which will be called after channel joins and is expected to return the initial state:
def init(_channel, _payload, _socket), do: {:ok, %{foo: "bar"}}
To send the state to the client, it is encoded and then serialized as JSON before being pushed over the channel. The encoding is handled by the c:LiveState.Encoder
protocol, the default implementation of which will handle all Elixir terms including converting structs to maps and removing the the __meta__
field for Ecto.Schema
structs. You can optionally implement the protocol to gain control over this encoding. It is important to understand that this encoding happens before JSON serialization and diffing occurs.
After the initial state is sent, subsequent state updates will be diffed against previous state using the jsondiff library and sent in jsonpatch format. You can, if you wish, provide an alternate implementation of the encoding and serialization process by specifying a module as the message_builder
option when calling use LiveState.Channel
. See c:LiveState.MessageBuilder
for an example.
For events emitted from the client, you implement the c:LiveState.Channel.handle_event/3
callback. If you need access the socket in your event handler, you may implement
c:LiveState.Channel.handle_event/4
.
def handle_event("add_todo", todo, %{todos: todos}) do
{:noreply, %{todos: [todo | todos]}}
end
c:LiveState.Channel.handle_event/3
receives the following arguments
- event name
- payload
- current state
And returns a tuple whose last element is the new state. It can also return one or many events to dispatch on the calling DOM Element:
def handle_event("add_todo_with_one_reply", todo, %{todos: todos}) do
{:reply, %Event{name: "reply_event", detail: %{foo: "bar"}}, %{todos: [todo | todos]}}
end
def handle_event("add_todo_with_two_replies", todo, %{todos: todos}) do
{:reply,
[
%Event{name: "reply_event1", detail: %{foo: "bar"}},
%Event{name: "reply_event2", detail: %{bing: "baz"}}
], %{todos: [todo | todos]}}
end
- Tutorial - A step by step guide to building an embedded app using LiveState
- API Docs are available in the hex package.
The front end library npm package that implements sending and receiving events, and subscribing to state changes. It also facilitates building Custom Elements that are backed by LiveState.
Live-templates is an npm package that allows you to connect a front end template to a LiveState channel. Because it doesn't require a custom element or any other javascript code, it is probably the easiest way to get started with LiveState.
A react hook for LiveState.
This is a Phoenix project that mainly provides integration tests for LiveState. It's also a great place to see examples of how to use LiveState.
There are several examples of full LiveState projects. This blog post covers building an embeddable custom element for a comments section. The relevant source code repos are:
- https://github.com/launchscout/live_state_comments
- https://github.com/launchscout/livestate-comments
This talk from ElixirConf 2022 also covers LiveState in detail. The code examples from the talk are in the repos below: