Use `temporary_assigns` for large lists in LiveView

almirsarajcic

almirsarajcic

3 hours ago

Sending entire lists over the socket on every LiveView update kills performance. Mark list assigns as temporary to send them once and drop them from socket state.

# ❌ FULL LIST - Sent on every update (slow and memory-heavy)
defmodule ProductsLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    products = load_products()  # 1000 products
    {:ok, assign(socket, products: products, filter: :all)}
  end

  def handle_event("toggle_filter", _params, socket) do
    # BUG: Entire products list sent over socket again!
    {:noreply, update(socket, :filter, fn f -> toggle(f) end)}
  end
end

# ✅ TEMPORARY ASSIGNS - Sent once, then dropped from memory
defmodule ProductsLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign(:filter, :all)
      |> assign(:products, load_products())  # 1000 products

    {:ok, socket, temporary_assigns: [products: []]}
  end

  def handle_event("toggle_filter", _params, socket) do
    # Products list NOT sent - only filter change transmitted
    {:noreply, update(socket, :filter, fn f -> toggle(f) end)}
  end
end

After the first render, temporary_assigns resets the :products assign to []. The rendered HTML stays in the browser, but the socket no longer holds the data.

How temporary assigns work

When you mark assigns as temporary:

  1. First render: Full list sent to browser, rendered into HTML
  2. Subsequent updates: Assign reset to default value (empty list)
  3. Socket state: No longer stores the large dataset
  4. DOM: Original HTML remains unchanged in browser
# Server memory footprint comparison
# Regular assigns:   socket.assigns.products = [1000 items]
# Temporary assigns: socket.assigns.products = []

# Network traffic comparison (per update)
# Regular assigns:   ~500 KB (1000 products × ~500 bytes each)
# Temporary assigns: ~1 KB (just the filter change)

Perfect for paginated or infinite scroll lists

When loading data incrementally, temporary assigns prevent accumulating all pages in memory:

defmodule InfiniteScrollLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign(:page, 1)
      |> stream(:items, load_page(1))

    {:ok, socket, temporary_assigns: [items: []]}
  end

  def handle_event("load-more", _params, socket) do
    next_page = socket.assigns.page + 1

    socket =
      socket
      |> update(:page, &(&1 + 1))
      |> stream(:items, load_page(next_page), at: -1)

    {:noreply, socket}
  end
end

When to use temporary assigns

Perfect for:

  • Large product catalogs or data tables
  • Chat messages (append-only streams)
  • Activity feeds or notifications
  • Search results that don’t change
  • Any list rendered once and rarely updated

Don’t use for:

  • Lists that need filtering/sorting in LiveView
  • Data you need to access in handle_event
  • Small lists (< 50 items) where overhead doesn’t matter
  • Dynamic lists where individual items update frequently

Accessing temporary data in events

If you need the data in event handlers, load it from the database instead:

defmodule ProductsLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign(:category_id, 1)
      |> assign(:products, load_products(1))

    {:ok, socket, temporary_assigns: [products: []]}
  end

  def handle_event("select", %{"id" => id}, socket) do
    # Products no longer in socket - fetch from DB
    product = Products.get_product!(id)

    socket =
      socket
      |> assign(:selected, product)
      |> assign(:products, load_products(socket.assigns.category_id))

    {:noreply, socket}
  end
end

Combine with streams for ultimate efficiency

For append-only lists, use stream/3 with temporary assigns:

defmodule ChatLive do
  use Phoenix.LiveView

  def mount(%{"room_id" => room_id}, _session, socket) do
    if connected?(socket) do
      MyAppWeb.Endpoint.subscribe("room:#{room_id}")
    end

    socket =
      socket
      |> assign(:room_id, room_id)
      |> stream(:messages, load_recent_messages(room_id))

    {:ok, socket, temporary_assigns: [messages: []]}
  end

  def handle_info({:new_message, message}, socket) do
    {:noreply, stream_insert(socket, :messages, message)}
  end
end

Use temporary_assigns whenever you render a large list that doesn’t need to live in socket state. Your LiveView will use less memory and respond faster to user interactions.