When adding items to a Phoenix.stream on mobile, the whole jumps to the bottom

I’m trying to put together a view that lists the items in the db. There are a lot of items so I ended up setting up a pagination system that adds more items to the list. But now on mobile it has this jerk when I scroll down.

I tried to capture that in a video, not sure I succeeded, here it is: iCloud Photos - Apple iCloud

I tried to implement pagination like I saw on the Phoenix documentation v1. I modified it to not replace the items but to keep adding to the list.

Any hunches on what is going on here and what I can do to improve it?

A few questions:

  • I’m not sure I’m using Phoenix.streams the correct way. Should I not even paginate, just hand the stream the entire list?
  • I thought the problem was caused by the fact that the parent element didn’t have phx-update=“stream”, but I added it and I still have this issue.

defmodule PluginProphetWeb.PluginLive.Index do
  use PluginProphetWeb, :live_view

  alias PluginProphet.Plugins
  alias PluginProphet.Plugins.Plugin
  alias PluginProphet.Contributors

  @per_page 50

  @impl true
  def mount(_params, _session, socket) do
    contributors = Contributors.list_contributors() |> Enum.reject(&(&1.name in ["", nil]))

    {:ok,
     socket
     |> assign(page: 1, per_page: @per_page, loading: false)
     |> assign(
       :form,
       to_form(%{"search" => "", "search_fields" => ["name"], "contributor_id" => ""})
     )
     |> assign(:contributors, contributors)
     |> stream(:plugins, [])}
  end

  @impl true
  def handle_params(params, _url, socket) do
    type =
      case params["type"] do
        "free" -> :free
        "paid" -> :paid
        "all" -> nil
        _ -> :paid
      end

    {:noreply,
     socket
     |> assign(type: type)
     |> apply_action(socket.assigns.live_action, params)
     |> load_plugins()}
  end

  @impl true
  def handle_event("next-page", _, socket) do
    {:noreply, socket |> update(:page, &(&1 + 1)) |> load_plugins()}
  end

  @impl true
  def handle_event("search", params, socket) do
    {:noreply,
     socket
     |> assign(page: 1)
     |> assign(:form, to_form(params))
     |> stream(:plugins, [], reset: true)
     |> load_plugins()}
  end

  defp apply_action(socket, :edit, %{"id" => id}) do
    socket
    |> assign(:page_title, "Edit Plugin")
    |> assign(:plugin, Plugins.get_plugin!(id))
  end

  defp apply_action(socket, :new, _params) do
    socket
    |> assign(:page_title, "New Plugin")
    |> assign(:plugin, %Plugin{})
  end

  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_title, "Listing Plugins")
    |> assign(:plugin, nil)
  end

  defp load_plugins(%{assigns: assigns} = socket) do
    %{type: type, page: page, per_page: per_page, form: form} = assigns

    search = form.params["search"]
    search_fields = form.params["search_fields"] || ["name"]
    contributor_id = form.params["contributor_id"]

    search_fields = Enum.map(search_fields, &String.to_existing_atom/1)

    opts = [
      search: search,
      search_fields: search_fields,
      type: type,
      contributor_id: contributor_id,
      page: page,
      per_page: per_page
    ]

    socket = assign(socket, loading: true)

    {total_count, plugins} = Plugins.list_plugins_and_total_count(opts)

    socket
    |> assign(loading: false, count: total_count)
    |> stream(:plugins, plugins, at: -1)
  end
end

The page


<div class=" min-w-min" phx-update="stream" id="plugins">
  <.table
    rows={@streams.plugins}
    pagination
    id="plugins_table"
    row_click={fn {_id, plugin} -> JS.navigate(~p"/plugins/#{plugin}") end}
  >
    <:col :let={{_id, plugin}} label="Name">
      <p class="w-[240px]">
        <%= plugin.name %>
      </p>
    </:col>
    
    <:col :let={{id, plugin}} label="Total installs">
      <p>
        <span id={"usage_#{id}"} phx-hook="FormatNumber"><%= plugin.usage_count %></span>
      </p>
    </:col>
    <:col :let={{id, plugin}} label="in production apps">
      <span id={"paid_installs_#{id}"} phx-hook="FormatNumber"><%= plugin.app_count %></span>
    </:col>
    <:col :let={{_id, plugin}} label="Maker">
      <span :if={plugin.contributor && plugin.contributor.name}>
        <%= plugin.contributor.name %>
      </span>
    </:col>

  </.table>
</div>

And the search function


"""
  @spec list_plugins_and_total_count(plugin_list_options()) :: {non_neg_integer(), [Plugin.t()]}
  def list_plugins_and_total_count(opts \\ []) do
    base_query = PluginQueries.build_base_query(opts)
    base_query = PluginQueries.apply_contributor_filter(base_query, opts)

    search_query =
      base_query
      |> PluginQueries.apply_search(opts)

    total_count =
      search_query
      |> exclude(:order_by)
      |> exclude(:preload)
      |> exclude(:select)
      |> select([p], count(p.id))
      |> Repo.one()

    plugins =
      search_query
      |> PluginQueries.apply_sorting(opts)
      |> PluginQueries.select_fields(opts)
      |> preload(:contributor)
      |> PluginQueries.apply_pagination(opts)
      |> Repo.all()

    {total_count, plugins}
  end