Use router `on_mount` hooks instead of duplicating LiveView mount logic

almirsarajcic

almirsarajcic

yesterday

Copy-pasting the same assigns across every LiveView mount/3 function creates maintenance nightmares. Router-level on_mount hooks run automatically before mount, eliminating duplication.

# ❌ DUPLICATED LOGIC - Copy-pasted across 15+ LiveViews
defmodule JobsLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    preferences = Preferences.for_user(socket.assigns.current_user)

    socket =
      socket
      |> assign(:theme, preferences.theme)
      |> assign(:timezone, preferences.timezone)
      |> assign(:page_title, "Jobs")

    {:ok, socket}
  end
end

defmodule CandidatesLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    preferences = Preferences.for_user(socket.assigns.current_user)

    socket =
      socket
      |> assign(:theme, preferences.theme)
      |> assign(:timezone, preferences.timezone)
      |> assign(:page_title, "Candidates")

    {:ok, socket}
  end
end

# ...repeat in 13 more LiveViews

Every LiveView duplicates preference loading. When you add a new preference field, you update 15+ files or risk inconsistent behavior.

# ✅ CENTRALIZED HOOK - Write once, apply everywhere
defmodule MyAppWeb.PreferencesHook do
  import Phoenix.LiveView

  def on_mount(:load_preferences, _params, _session, socket) do
    preferences = Preferences.for_user(socket.assigns.current_user)

    socket =
      socket
      |> assign(:theme, preferences.theme)
      |> assign(:timezone, preferences.timezone)

    {:cont, socket}
  end
end
# Router - Apply to all LiveViews in live_session
scope "/app", MyAppWeb do
  pipe_through [:browser, :require_authenticated_user]

  live_session :authenticated,
    on_mount: [
      {MyAppWeb.UserAuth, :ensure_authenticated},
      {MyAppWeb.PreferencesHook, :load_preferences}
    ] do
    live "/jobs", JobsLive, :index
    live "/candidates", CandidatesLive, :index
    live "/analytics", AnalyticsLive, :index
  end
end
# LiveViews - Clean mount functions
defmodule JobsLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    # :theme and :timezone already loaded by hook
    {:ok, assign(socket, :page_title, "Jobs")}
  end
end

defmodule CandidatesLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    # :theme and :timezone already loaded by hook
    {:ok, assign(socket, :page_title, "Candidates")}
  end
end

Now changing preference loading logic requires updating one hook module instead of 15+ LiveViews.

Hook execution order

Hooks run in the order specified, before each LiveView’s mount/3:

live_session :authenticated,
  on_mount: [
    {MyAppWeb.UserAuth, :ensure_authenticated},    # 1. Verify authentication
    {MyAppWeb.PreferencesHook, :load_preferences}, # 2. Load preferences (needs current_user)
    {MyAppWeb.AnalyticsHook, :track_pageview}      # 3. Track analytics
  ] do
  live "/dashboard", DashboardLive, :index
end

Each hook returns {:cont, socket} to continue, or {:halt, socket} to stop (useful for redirects in auth checks).

Common use cases for on_mount hooks

Authentication/Authorization:

def on_mount(:ensure_admin, _params, _session, socket) do
  if socket.assigns.current_user.role == :admin do
    {:cont, socket}
  else
    {:halt, redirect(socket, to: "/")}
  end
end

Feature flags:

def on_mount(:load_features, _params, _session, socket) do
  features = FeatureFlags.for_user(socket.assigns.current_user)
  {:cont, assign(socket, :features, features)}
end

Analytics tracking:

def on_mount(:track_pageview, _params, _session, socket) do
  if connected?(socket) do
    Analytics.track_pageview(socket.assigns.current_user)
  end

  {:cont, socket}
end

Hooks keep your LiveView mount/3 functions focused on page-specific logic while centralizing cross-cutting concerns at the router level.