Strange behavior of Liveview streams when using reset: true

I am trying to use the main branch of phoenix liveview to test the new reset feature on streams
In my mix.exs I have

{:phoenix_live_view,
       github: "phoenixframework/phoenix_live_view", branch: "main", override: true}

I have created a simple liveview example for a phonebook.
I have a list of names and an index at the top. Clicking on the index should load the contacts that start with the Index letter.
Whenever the index letter is clicked, i load all the relevant contacts, put them in the stream and set the reset flag to true.
This is working fine.

The Problem

Clicking on each contact opens up a modal with the contact details.
This works fine for the first time. Click on the modal, the modal appears.

But whenever the stream gets reset (after clicking a index letter), clicking on the contact brings up the modal, but the stream gets cleared and all contacts disappear.
I am not able to figure out why this is happening.
I tried to replace the stream, with normal assigns and everything works like it should (the modal appears and the contacts do not get cleared).

Can some one please advise as to why is this happening?

Full code is below

defmodule StreamsTestWeb.UserLive.Index do
  use StreamsTestWeb, :live_view

  @impl true
  def render(assigns) do
    ~H"""
    <div class="h-20 flex items-center space-x-12 border-b border-zinc-200 px-4 lg:px-8">
      <div class="text-lg font-bold">Users</div>
    </div>
    <div class="tabs flex items-center gap-2 text-xs font-medium px-r lg:px-8 mt-8">
      <.tab :for={tab <- @tabs} tab={tab} selected_tab={@selected_tab} />
    </div>
    <div :if={@term != ""} class="flex items-center gap-4 text-sm px-4 lg:px-8 mt-6">
      <div class="text-zinc-500">Searching <em class="font-bold text-zinc-700"><%= @term %></em></div>
      <button
        type="button"
        class="flex items-center gap-2 text-xs text-zinc-400 hover:text-zinc-600"
        phx-click="clear_search"
      >
        <.icon name="hero-x-circle" class="w-5 h-5" />
      </button>
    </div>
    <div class="px-4 lg:px-8">
      <.table
        id="users"
        rows={@streams.users}
        row_click={fn {_id, u} -> JS.push("show_user", value: %{id: u.id}) end}
      >
        <:col :let={{_id, s}} label="Name">
          <%= s.name %>
        </:col>
      </.table>
    </div>
    <div id="modal_box">
      <.modal :if={@user} id="detail-modal" show on_cancel={JS.push("hide_user")}>
        <.header><%= @user.name %></.header>
      </.modal>
    </div>
    """
  end

  @impl true
  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign(:tabs, get_tabs())
      |> assign(:selected_tab, "All")
      |> assign(:term, "")
      |> assign(:user, nil)
      |> stream(:users, get_users)

    {:ok, stream(socket, :users, [])}
  end

  attr(:tab, :string, required: true)
  attr(:selected_tab, :string, required: true)

  def tab(assigns) do
    ~H"""
    <div
      class={[
        "w-6 h-6 inline-flex items-center justify-center rounded cursor-pointer",
        @tab == @selected_tab && "text-green-700 bg-green-100",
        @tab != @selected_tab && "text-zinc-500 bg-transparent hover:text-zinc-700 hover:bg-zinc-100"
      ]}
      phx-click="load_contacts"
      phx-value-index={@tab}
    >
      <%= @tab %>
    </div>
    """
  end

  @impl true
  def handle_event("load_contacts", %{"index" => index}, socket) do
    users = get_users() |> Enum.filter(fn s -> String.starts_with?(s.name, index) end)

    socket =
      socket
      |> assign(:selected_tab, index)
      |> assign(:term, "")
      |> assign(:user, nil)
      |> stream(:users, users, reset: true)

    {:noreply, socket}
  end

  def handle_event("show_user", %{"id" => id}, socket) do
    socket =
      assign(
        socket,
        :user,
        Enum.find(get_users, fn u -> u.id == id end)
      )

    {:noreply, socket}
  end

  def handle_event("hide_user", _params, socket) do
    socket =
      assign(
        socket,
        :user,
        nil
      )

    {:noreply, socket}
  end

  defp get_tabs() do
    [
      "A",
      "B",
      "C",
      "D",
      "E",
      "F",
      "G",
      "H",
      "I",
      "J",
      "K",
      "L",
      "M",
      "N",
      "O",
      "P",
      "Q",
      "R",
      "S",
      "T",
      "U",
      "V",
      "W",
      "X",
      "Y",
      "Z"
    ]
  end

  def get_users() do
    [
      %{id: 1, name: "A Name"},
      %{id: 2, name: "B Name"},
      %{id: 3, name: "C Name"},
      %{id: 4, name: "D Name"},
      %{id: 5, name: "E Name"},
      %{id: 6, name: "F Name"},
      %{id: 7, name: "G Name"},
      %{id: 8, name: "H Name"},
      %{id: 9, name: "I Name"},
      %{id: 10, name: "J Name"}
    ]
  end
end

1 Like

A video to better understand the issue

This is happening

  1. Do some action to reset the stream using the reset: true option in the stream function. The stream would update and the DOM would be changed accordingly.
  2. Do any other action that updates the socket (other than the stream), if that causes dom changes, then the stream would get cleared.

One workaround is to make stream a part of the assigns by doing a delete operation for a non existent item.

def handle_event("show_user", %{"id" => id}, socket) do
    socket =
      assign(
        socket,
        :user,
        Enum.find(get_users, fn u -> u.id == id end)
      )
      |> stream_delete(:users, %{id: -1})

    {:noreply, socket}
  end

Doing this makes the contacts not go away.

But i am still not able to understand why is this happening?

Are you positive the table’s tbody has phx-update="stream" ?

1 Like

Yes.

<tbody id="users" phx-update="stream" class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700">
      
    
      <tr id="users-32" class="group hover:bg-zinc-50" data-phx-stream="0">
        <td class="relative p-0 ">
          <div class="block py-4 pr-6 ">
            <span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl"></span>
            <span class="relative font-semibold text-zinc-900">
              
      Ajay-Accounts
    
            </span>
          </div>
        </td>
      </tr>
    </tbody>

Oh, I think this may be related:

This is fixed on main. Thank you!

3 Likes

Thank you so much @chrismccord
More power to you :pray: