Use `temporary_assigns` for large lists in LiveView

almirsarajcic

almirsarajcic

1 month ago

0 comments

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.

Comments (0)

Sign in with GitHub to join the discussion