Simple Phoenix LiveView App: PubSub & Index

Last time we used Phoenix generators to scaffold out our users and content.

In this episode, we'll start converting the User pages into Live Views. First is adding some PubSub functions to the Accounts Context for user-related LiveViews. Then we'll write a LiveView for the users index page.

Setting up PubSub

Since Phoenix Framework already includes PubSub, functionality, we can let it do the heavy lifting and just make a few helpers in our Accounts context. At the top of accounts.ex, add the following:

  @topic inspect(__MODULE__)

  def subscribe do
    Phoenix.PubSub.subscribe(Reactor.PubSub, @topic)
  end

  def subscribe(user_id) do
    Phoenix.PubSub.subscribe(Reactor.PubSub, @topic <> "#{user_id}")
  end

In the code above, @topic is just the name of the module, i.e., "Accounts". Clients subscribing to the list of all users, will just use that as the PubSub channel name. Clients subscribing to a specific user, will subscribe to a channel name of "Accounts" appended by the user's id.

Then, at the bottom of the Accounts module, add a notify_subscribers function:

  defp notify_subscribers({:ok, result}, event) do
    Phoenix.PubSub.broadcast(Reactor.PubSub, @topic, {__MODULE__, event, result})

    Phoenix.PubSub.broadcast(
      Reactor.PubSub,
      @topic <> "#{result.id}",
      {__MODULE__, event, result}
    )

    {:ok, result}
  end

  defp notify_subscribers({:error, reason}, _), do: {:error, reason}

This function takes a tuple returned by Ecto—either :ok and some data or :error and a reason—as the first argument and an event name. It broadcasts the data or error along with the event name to all subscribers. Finally, the function returns its first argument unchanged, so we can drop it into CRUD pipelines without changing their output.

Add it to the end of all context functions that involve a write:

  def create_user(attrs \\ %{}) do
    %User{}
    |> User.changeset(attrs)
    |> Repo.insert()
    |> notify_subscribers([:user, :created])
  end

  def update_user(%User{} = user, attrs) do
    user
    |> User.changeset(attrs)
    |> Repo.update()
    |> notify_subscribers([:user, :updated])
  end

  def delete_user(%User{} = user) do
    Repo.delete(user)
    |> notify_subscribers([:user, :deleted])
  end

Create a LiveView for Index

Create a new file reactor_web/live/user_live/index.ex with the following contents:

defmodule ReactorWeb.UserLive.Index do
  use Phoenix.LiveView
  alias Reactor.Accounts
  alias ReactorWeb.UserView

  def render(assigns), do: UserView.render("index.html", assigns)

  def mount(_params, _session, socket) do
    if connected?(socket), do: Reactor.Accounts.subscribe()
    {:ok, fetch(socket)}
  end

  defp fetch(socket) do
    users = Accounts.list_users()
    assign(socket, users: users, page_title: "Listing Users")
  end

  def handle_event("delete_user", %{"id" => user_id}, socket) do
    Accounts.get_user!(user_id)
    |> Accounts.delete_user()

    {:noreply, socket}
  end

  def handle_info({Accounts, [:user, _], _}, socket) do
    {:noreply, fetch(socket)}
  end
end

The handle_info function listens to the notifications broadcast by accounts, and regardless of whether the event, is [:user, :created], [:user, :updated], [:user, :deleted] or :user anything, it fetches a fresh listing of users from the database.

The handle_event function listens to the delete_user event, which is sent from the "index.html.leex" live template.

The full series playlist for Simple Phoenix LiveView App on YouTube is slightly ahead of what's published here at the moment.

(Source code available for premium members)

Back to index