DEV Community

Cover image for Migrating to Phoenix Liveview Streams
Ahsan Nabi Dar
Ahsan Nabi Dar

Posted on

Migrating to Phoenix Liveview Streams

Phoenix released 1.7.0 with Liveview 0.18.16 and introduced Streams for liveview. At first it was not clear to me how to replace my current prepend logic from the published blog So I reached out to its creator of phoenix Chris McCord on twitter and he not only responded, even took out time to review my code and share what would be the way to get it done with stream. This is why elixir community is awesome.

tweet thread

The thing that was not clear from the blog post introducing streams was how would one send additional data over streams that is part of the page but not the actual stream.My apps stream dashboard used prepend and it clearly could benefit from the streams and was an obvious choice for upgrade with streams release.

Here is how the old liveview prepend code

defmodule KyubiWeb.Stream.LiveView do
  use KyubiWeb, :live_view

  @cache :kyubi_cachex
  def mount(_params, _session, socket) do
    if connected?(socket), do: KyubiWeb.Endpoint.subscribe("dns")
    {:ok, init_stream(socket), temporary_assigns: [cards: []]}
  end

  def handle_info(%{event: "stream", payload: query}, socket) do
    {:noreply,
     assign(
       socket,
       cards: Kyubi.Stream.Card.populate(query, socket.id),
       term: get_filter_term(socket.id),
       kyubi_node: Kyubi.Utility.Helper.nodename()
     )}
  end

  def handle_event(
        _event,
        %{"filter" => %{"term" => incoming_query_filter}},
        socket
      ) do
    if String.trim(incoming_query_filter) == "",
      do: Cachex.del(@cache, socket.id),
      else: Cachex.put(@cache, socket.id, {:ok, incoming_query_filter})

    {:noreply,
     assign(
       socket,
       cards: Kyubi.Stream.Card.new(),
       term: incoming_query_filter
     )}
  end

  def init_stream(socket) do
    assign(
      socket,
      cards: Kyubi.Stream.Card.new(),
      term: get_filter_term(socket.id),
      kyubi_node: Kyubi.Utility.Helper.nodename()
    )
  end

  def get_filter_term(socket_id) do
    case Cachex.get(@cache, socket_id) do
      {:ok, {:ok, filter}} ->
        filter

      {:ok, nil} ->
        ""
    end
  end
end

Enter fullscreen mode Exit fullscreen mode
 <div class="row mb-3 mx-3">
    <div id="dns-stream" phx-update="prepend" class="mb-3 mx-3 col">
      <%= for query <- @cards.queries do %>
        <p id={query.resource_event_id} />
        <div
          id={"dns-query-#{query.resource_status}%>-#{query.resource_event_id}"}
          class={"card text-white bg-dark
          border border-#{@cards.label.message_type} mb-3 mr-3 ml-3"}
        >
          <KyubiWeb.Components.Stream.card
            query={query}
            label={@cards.label}
            favicon={@cards.favicon}
            ad_tracker={@cards.ad_tracker}
          />
          <p class="card-text">
            <small class="text-muted">
            <%= query.client_user_agent %> <%= query.client_ip %>,
            <%= query.client_city %>
            </small>, &nbsp;<img
              src={~p"/kyubi/images/flags/#{@cards.country_flag_code}"}
              height="14"
            />
          </p>
        </div>
      <% end %>
    </div>
  </div>

Enter fullscreen mode Exit fullscreen mode

Here is the updated liveview code based on streams

defmodule KyubiWeb.Stream.LiveView do
  use KyubiWeb, :live_view

  @cache :kyubi_cachex
  def mount(_params, _session, socket) do
    if connected?(socket), do: KyubiWeb.Endpoint.subscribe("dns")
    {:ok, init_stream(socket)}
  end

  def handle_info(%{event: "stream", payload: query}, socket) do
    {:noreply,
     assign(
       socket,
       term: get_filter_term(socket.id),
       kyubi_node: Kyubi.Utility.Helper.nodename()
     )
     |> stream_insert(:cards, Kyubi.Stream.Card.populate(query, socket.id), at: 0)}
  end

  def handle_event(
        _event,
        %{"filter" => %{"term" => incoming_query_filter}},
        socket
      ) do
    if String.trim(incoming_query_filter) == "",
      do: Cachex.del(@cache, socket.id),
      else: Cachex.put(@cache, socket.id, {:ok, incoming_query_filter})

    {:noreply,
     assign(
       socket,
       term: incoming_query_filter
     )
     |> stream_insert(:cards, Kyubi.Stream.Card.new(), at: 0)}
  end

  def init_stream(socket) do
    assign(
      socket,
      term: get_filter_term(socket.id),
      kyubi_node: Kyubi.Utility.Helper.nodename()
    )
    |> stream(:cards, [Kyubi.Stream.Card.new()])
  end

  def get_filter_term(socket_id) do
    case Cachex.get(@cache, socket_id) do
      {:ok, {:ok, filter}} ->
        filter

      {:ok, nil} ->
        ""
    end
  end
end

Enter fullscreen mode Exit fullscreen mode
 <div class="row mb-3 mx-3">
    <div id="dns-stream" phx-update="prepend" class="mb-3 mx-3 col">
      <%= for query <- @cards.queries do %>
        <p id={query.resource_event_id} />
        <div
          id={"dns-query-#{query.resource_status}%>-#{query.resource_event_id}"}
          class={"card text-white bg-dark
          border border-#{@cards.label.message_type} mb-3 mr-3 ml-3"}
        >
          <KyubiWeb.Components.Stream.card
            query={query}
            label={@cards.label}
            favicon={@cards.favicon}
            ad_tracker={@cards.ad_tracker}
          />
          <p class="card-text">
            <small class="text-muted">
            <%= query.client_user_agent %> <%= query.client_ip %>,
            <%= query.client_city %>
            </small>, &nbsp;<img
              src={~p"/kyubi/images/flags/#{@cards.country_flag_code}"}
              height="14"
            />
          </p>
        </div>
      <% end %>
    </div>
  </div>

Enter fullscreen mode Exit fullscreen mode

Streams definitely have a better developer experience compared to the append and prepend design. At times the release docs or blog posts don't cover all the use cases that are out there implemented and stalls upgrades. The elixir community makes it possible for the developers to adopt the latest changes seamlessly

Top comments (2)

Collapse
 
sabit990928 profile image
Sabit Rakhim

Shouldn't you update your template to be <div id="dns-stream" phx-update="stream" instead of <div id="dns-stream" phx-update="prepend"?

Collapse
 
daniel4lm profile image
Daniel Molnar

Hi @darnahsan , this is a great new feature in our Elixir/Phoenix community. In my side project I also replaced temporary assigns with streams and sure it's working fine as espected. One thing I miss is ability to clear/reset whole stream and insert more items at once. For example I have a form on the same page with list(that form filters list according to some criteria). When I need to reset the list, for now I only can collect doom id's from items(using the phx-hook), assign them to the socket and delete all items from stream(for loop) by using stream_delete_by_dom_id/3.