Using stream_delete removes all the items from the list

I can’t figure out why when deleting an article from the list:

defmodule MyAppWeb.ArticleLive.Index
...

on_mount({MyAppWeb.UserAuth, :mount_current_user})

  @impl true
  def mount(_params, _session, socket) do
    {:ok, stream(socket, :articles, Articles.list_articles()}
  end

@impl true
  def handle_event("delete", %{"id" => id}, socket) do
    article = Articles.get_article!(id)
    current_user_id = socket.assigns.current_user.id

    if article.author_id != current_user_id do
      raise """
        An unauthorized author tried to delete article #{article.id} by author #{current_user_id}
      """
    end

    {:ok, _} = Articles.delete_article(article)

    {:noreply, stream_delete(socket, :articles, article)}
  end

Here is how the list of articles is displayed in index.html.heex:

...
<%= if Enum.count(@streams.articles) == 0 do %>
    <div class="mt-20">
      <MyAppWeb.Component.EmptyState.show text="You have no articles yet." image="wall-post.svg" />
    </div>
  <% else %>
    <.table
      id="articles"
      rows={@streams.articles}
      row_click={fn {_id, article} -> JS.navigate(~p"/articles/#{article}") end}
    >
      <:col :let={{_id, article}} label="Title"><%= article.title %></:col>
      <:col :let={{_id, article}} label="Content">
        <%= String.slice(article.content, 0, 200) %>
      </:col>
      <:action :let={{_id, article}}>
        <div class="sr-only">
          <.link navigate={~p"/articles/#{article}"}>Show</.link>
        </div>
        <.link patch={~p"/articles/#{article}/edit"}>Edit</.link>
      </:action>
      <:action :let={{id, article}}>
        <.link
          phx-click={JS.push("delete", value: %{id: article.id}) |> hide("##{id}")}
          data-confirm="Are you sure?"
        >
          Delete
        </.link>
      </:action>
    </.table>
  <% end %>
...

When I click on the link to delete an article and confirm the deletion, no more articles are displayed on the page. But if I refresh the page, I have the list of articles without the deleted one.
When comparing the above content with another basic Phoenix app, everything seems to be correct.

Any ideas?

It’s the Enum.count(@streams.articles). Streams are not held on the server by design so that key is emptied when the data is sent to the client.

Hmm, removing the ìf` clause fixed the problem. Here is how the empty state component looks like:

defmodule MyAppWeb.Component.EmptyState do
  use MyAppWeb, :html

  attr(:text, :string, required: true)
  attr(:image, :string, required: true)

  def show(assigns) do
    ~H"""
    <div>
      <h2 class="text-2xl font-semibold tracking-tight sm:text-center sm:text-lg">
        <%= @text %>
      </h2>
      <div class="w-full max-w-xs mx-auto mt-10">
        <img
          src={~p"/images/#{@image}"}
          alt=""
          class="w-full rounded-xl ring-1 ring-gray-400/10 max-w-none md:-ml-4 lg:-ml-0"
        />
      </div>
    </div>
    """
  end
end

So this kind of check <%= if Enum.count(@streams.articles) == 0 do %> will never work then? If so, what would be better - remove it completely or replace it with something other? More of that, we assign rows={@streams.articles} later on to the rows. How is it possible that calling to Enum.count will fail then?

Correct, it will never work. It’s not something I’ve dealt with before but I’m pretty sure there was some recent discussion about this. I’m on my phone otherwise I’d try and find it. Would be regarding “message when stream empty” or something like that.

The former works because of using phx-update="stream" to tell the client to not remove things on update if they’re not present in @stream.articles, but only when the stream includes an explicit deletion. That allows for dropping any data in @stream.articles after each render. That’s the whole reason why streams exist in the first place - to not need to retain the collection on the server side.

2 Likes

OK, thank you very much. I’ll try to search a thread for a message when stream empty or smth like that :).

:sweat_smile:

I’m on a laptop now I’m sure you found it as I searched that and it was the first result but here it is!

1 Like

Actually I have the same request as your question. Does anyone know how to remove all the items from the liveview stream? I was thinking about stream(smth, []), but it didn’t work as expected.

Maybe I need to try to pass that at option.

The old school answer is to have the container of the stream have some id value that you set from an assign eg id={"items-#{@item_counter}"} and then you increment the :item_counter assign when you want to reset the stream.

Not sure if there is a more first class answer these days or not yet. There is a better option now! Phoenix.LiveView — Phoenix LiveView v0.19.5 reset option on stream/4

1 Like

There’s a reset option on stream/4

3 Likes

You’re right. There’s such an option, I forgot about it.

stream(socket, :songs, [], reset: true)

Thanks. You too, @benwilson512, quite an interesting idea with increment.