Phoenix.Sync.LiveView (Phoenix.Sync v0.6.0)

View Source

Swap out Phoenix.LiveView.stream/3 for Phoenix.Sync.LiveView.sync_stream/4 to automatically keep a LiveView up-to-date with the state of your Postgres database:

defmodule MyWeb.MyLive do
  use Phoenix.LiveView
  import Phoenix.Sync.LiveView

  def mount(_params, _session, socket) do
    {:ok, sync_stream(socket, :todos, Todos.Todo)}
  end

  def handle_info({:sync, event}, socket) do
    {:noreply, sync_stream_update(socket, event)}
  end
end

See `sync_stream/4` for more details.

Summary

Functions

Maintains a LiveView stream from the given Ecto query.

Handle Electric events within a LiveView.

Types

component_event()

@opaque component_event()

event()

@type event() :: replication_event() | state_event()

replication_event()

@opaque replication_event()

root_event()

@opaque root_event()

state_event()

@type state_event() :: {atom(), :loaded} | {atom(), :live}

stream_option()

@type stream_option() ::
  {:at, integer()}
  | {:limit, pos_integer()}
  | {:reset, boolean()}
  | {:client, struct()}

stream_options()

@type stream_options() :: [stream_option()]

Functions

sync_stream(socket, name, query, opts \\ [])

@spec sync_stream(
  socket :: Phoenix.LiveView.Socket.t(),
  name :: atom() | String.t(),
  query :: Ecto.Queryable.t(),
  opts :: stream_options()
) :: Phoenix.LiveView.Socket.t()

Maintains a LiveView stream from the given Ecto query.

  • name The name to use for the LiveView stream.
  • query An Ecto query that represents the data to stream from the database.

For example:

def mount(_params, _session, socket) do
  socket =
    Phoenix.Sync.LiveView.sync_stream(
      socket,
      :admins,
      from(u in Users, where: u.admin == true)
    )
  {:ok, socket}
end

This will subscribe to the configured Electric server and keep the list of :admins in sync with the database via a Phoenix.LiveView stream.

Updates will be delivered to the view via messages to the LiveView process.

To handle these you need to add a handle_info/2 implementation that receives these:

def handle_info({:sync, event}, socket) do
  {:noreply, Phoenix.Sync.LiveView.sync_stream_update(socket, event)}
end

See the docs for Phoenix.LiveView.stream/4 for details on using LiveView streams.

Lifecycle Events

Most {:sync, event} messages are opaque and should be passed directly to the sync_stream_update/3 function, but there are two events that are outside Electric's replication protocol and designed to be useful in the LiveView component.

  • {:sync, {stream_name, :loaded}} - sent when the Electric event stream has passed from initial state to update mode.

    This event is useful to show the stream component after the initial sync. Because of the streaming nature of Electric Shapes, the initial sync can cause flickering as items are added, removed and updated.

    E.g.:

    # in the LiveView component
    def handle_info({:sync, {_name, :live}}, socket) do
      {:noreply, assign(socket, :show_stream, true)}
    end
    
    # in the template
    <div phx-update="stream" class={unless(@show_stream, do: "opacity-0")}>
      <div :for={{id, item} <- @streams.items} id={id}>
        <%= item.value %>
      </div>
    </div>
  • {:sync, {stream_name, :live}} - sent when the Electric stream is in live mode, that is the initial state has loaded and the client is up-to-date with the database and is long-polling for new events from the Electric server.

If your app doesn't need this extra information, then you can ignore them and just have a catch-all callback:

def handle_info({:sync, event}, socket) do
  {:noreply, Phoenix.Sync.LiveView.sync_stream_update(socket, event)}
end

Phoenix.Sync.LiveView.sync_stream_update will just ignore the lifecycle events.

Sub-components

If you register your Electric stream in a sub-component you will still receive Electric messages in the LiveView's root/parent process.

Phoenix.Sync handles this for you by encapsulating component messages so it can correctly forward on the event to the component.

So in the parent LiveView process you handle the :sync messages as above:

defmodule MyLiveView do
  use Phoenix.LiveView

  def render(assigns) do
    ~H"""
    <div>
      <.live_component id="my_component" module={MyComponent} />
    </div>
    """
  end

  # We setup the Electric sync_stream in the component but update messages will
  # be sent to the parent process.
  def handle_info({:sync, event}, socket) do
    {:noreply, Phoenix.Sync.LiveView.sync_stream_update(socket, event)}
  end
end

In the component you must handle these events in the Phoenix.LiveComponent.update/2 callback:

defmodule MyComponent do
  use Phoenix.LiveComponent

  def render(assigns) do
    ~H"""
    <div id="users" phx-update="stream">
      <div :for={{id, user} <- @streams.users} id={id}>
        <%= user.name %>
      </div>
    </div>
    """
  end

  # Equivalent to the `handle_info({:sync, {stream_name, :live}}, socket)` callback
  # in the parent LiveView.
  def update(%{sync: {_stream_name, :live}}, socket) do
    {:ok, socket}
  end

  # Equivalent to the `handle_info({:sync, event}, socket)` callback
  # in the parent LiveView.
  def update(%{sync: event}, socket) do
    {:ok, Phoenix.Sync.LiveView.sync_stream_update(socket, event)}
  end

  def update(assigns, socket) do
    {:ok, Phoenix.Sync.LiveView.sync_stream(socket, :users, User)}
  end
end

Keyword-based Shapes

Ecto is not required to use sync_stream/4. Keyword-based shapes are possible but work a little differently.

def mount(_params, _session, socket) do
  socket =
    Phoenix.Sync.LiveView.sync_stream(
      socket,
      :admins,
      table: "users",
      where: "admin = true"
    )
  {:ok, socket}
end

Without an underlying Ecto.Schema module to map the stream values, you will receive simple map values with string keys (plus a special __sync_key__ value used for the live view dom_id function.

So to use these in your template, you should be careful to use the value["key"] syntax to retrieve values. Using a keyword-based shape, The first example above becomes:

  <div phx-update="stream">
    <div :for={{id, item} <- @streams.items} id={id}>
      <%= item["value"] %>
    </div>
  </div>

sync_stream_update(socket, event, opts \\ [])

@spec sync_stream_update(Phoenix.LiveView.Socket.t(), event(), Keyword.t()) ::
  Phoenix.LiveView.Socket.t()

Handle Electric events within a LiveView.

def handle_info({:sync, event}, socket) do
  {:noreply, Phoenix.Sync.LiveView.sync_stream_update(socket, event, at: 0)}
end

The opts are passed to the Phoenix.LiveView.stream_insert/4 call.