`assign_new/3` prevents expensive recomputations in LiveView

almirsarajcic

almirsarajcic

10 hours ago

0 comments

Calling assign/2 recalculates values every time the socket updates, even if the data hasn’t changed. Use assign_new/3 to compute expensive operations only once and preserve them across LiveView updates.

# ✅ COMPUTE ONCE - Database query runs only on first access
defmodule DashboardLive do
  use Phoenix.LiveView

  import Ecto.Query

  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign_new(:categories, fn -> load_categories_from_db() end)
      |> assign(selected: nil, show_filters: false)

    {:ok, socket}
  end

  def handle_event("select_category", %{"id" => id}, socket) do
    {:noreply, assign(socket, selected: id)}
  end

  def handle_event("toggle_filter", _params, socket) do
    {:noreply, update(socket, :show_filters, &(!&1))}
  end

  defp load_categories_from_db do
    Repo.all(from c in Category, order_by: [asc: c.name])
  end
end

Without assign_new/3, every event triggers a database query for categories that never change during the session. With 20 category selections, that’s 20 identical queries.

# ❌ REPEATED WORK - Database query runs on every event
defmodule DashboardLive do
  use Phoenix.LiveView

  import Ecto.Query

  def mount(_params, _session, socket) do
    categories = load_categories_from_db()
    {:ok, assign(socket, categories: categories, selected: nil, show_filters: false)}
  end

  def handle_event("select_category", %{"id" => id}, socket) do
    # Query again!
    categories = load_categories_from_db()
    {:noreply, assign(socket, categories: categories, selected: id)}
  end

  def handle_event("toggle_filter", _params, socket) do
    # Query again!
    categories = load_categories_from_db()
    {:noreply, update(socket, :show_filters, &(!&1))}
  end

  defp load_categories_from_db do
    Repo.all(from c in Category, order_by: [asc: c.name])
  end
end

Now load_categories_from_db/0 runs only once when the socket is first created. All subsequent events reuse the cached value from socket.assigns.categories.

How assign_new/3 works

assign_new/3 checks if the key already exists in socket assigns:

  • Key exists: Skip the function, keep existing value
  • Key missing: Run the function, store the result

This is perfect for:

  • Database queries for static dropdown data
  • API calls for configuration
  • Expensive computations that don’t change per-session
  • User preferences loaded once at mount

Works with inline child LiveViews

When a parent LiveView renders a child LiveView inline via live_render/3, assign_new/3 in the child checks the parent’s assigns first. If the parent already has the key, the function is skipped entirely — no redundant computation.

The child must also call assign_new/3 with the same key. The data isn’t injected automatically; the child explicitly opts in by requesting it.

# Parent LiveView loads expensive data once
defmodule ParentLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    socket = assign_new(socket, :app_config, fn -> load_app_config() end)
    {:ok, socket}
  end
end

# Child LiveView rendered inline via live_render/3 reuses parent's value
defmodule ChildLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    # assign_new checks parent's assigns first — skips the function if parent already has :app_config
    socket = assign_new(socket, :app_config, fn -> load_app_config() end)
    {:ok, socket}
  end
end

This only applies to inline rendering (live_render/3). When navigating between LiveViews — even within the same live_session — each LiveView mounts fresh with its own process and assign_new/3 has no access to any prior LiveView’s assigns.

Use assign_new/3 for any data that’s expensive to compute and stable within a session. Your LiveView will be faster and your database will thank you.

assign_new/3 docs

Comments (0)

Sign in with GitHub to join the discussion