jonnifyelixir
F6 / F6.06 / mount

F6.06 · part 1 of 3

mount & assigns

A LiveView is a stateful process connected to the browser over a socket, and mount/3 is the one callback that gives it its first state.

The socket is the process state. mount/3 receives the route params, the session, and that socket, and returns {:ok, socket} — the LiveView analogue of a GenServer init/1 from F5. It runs in two stages: a disconnected HTTP render for the first paint, then a connected render once the socket is established, so connected?/1 guards the side effects that must happen exactly once. Assigns go on with assign/3, or with assign_new/4 when a value should be computed only if it is not already present.

The socket is the state

A LiveView is an OTP process — the same long-lived, message-driven actor F5 built the Portal engine from. Its state is held in one value, the socket, and the part of the socket an author writes to is socket.assigns: a plain map from atom keys to values. Everything the view renders is read from assigns; everything an event changes is written back to assigns. The rest of the socket carries connection bookkeeping that the framework owns.

socket
The LiveView process state: a %Phoenix.LiveView.Socket{} struct that holds the assigns plus connection metadata.
assigns
The map inside the socket an author reads and writes — the data the render is a function of.
mount/3
The first callback: it receives params, session, and socket, and returns {:ok, socket} with the initial assigns set.
connected?/1
A predicate on the socket: false during the disconnected first paint, true once the socket is live.

Naming a function mount/3 states two facts at once: a callback named mount, taking three arguments — params, session, socket. The first two are inputs from the request; the third is the state to be initialised and returned.

The two-stage connect

A LiveView is reached twice. The first request is ordinary HTTP: the router runs mount/3, renders the markup once, and sends static HTML — fast to paint and indexable by a crawler, with no socket open yet. The browser then opens a WebSocket and the LiveView mounts a second time, now connected; from that point the process stays alive and exchanges diffs. Both stages run the same mount/3, so the difference between them is exactly what connected?(socket) reports. Move the control through the connect.

The two-stage connect · drag through it

1 / 2
STAGE 1 · DISCONNECTED HTTP mount/3 runs · renders once static HTML → first paint connected?(socket) == false no socket yet · side effects skipped fast, indexable, stateless paint WebSocket opens STAGE 2 · CONNECTED SOCKET mount/3 runs again · live process stays alive · diffs connected?(socket) == true subscriptions & timers start here, once stateful · until the socket drops

stage: disconnected HTTP

connected?(socket): false

One mount/3, two arrivals. The first paints HTML and skips anything with an effect; the second is the live process, and it is the only place a subscription or a timer should start — which is exactly the question connected?/1 answers.

assign and assign_new

State is written with assign/3: assign(socket, :key, value) returns a new socket with that key set, in the immutable style of every F2 update — the old socket is unchanged. assign_new/4 is the conditional form: assign_new(socket, :key, fun) runs fun and sets the key only when the key is absent, and otherwise keeps the existing value and never calls fun. That guard matters across the two-stage connect: a value the disconnected stage already placed (a current user handed down from a plug, say) is reused on the connected mount instead of being recomputed.

# assign/3 — set a key unconditionally, returning a new socket
socket = assign(socket, :progress, 42)
socket.assigns.progress
# => 42

# assign_new/4 — compute and set only if the key is absent
socket = assign_new(socket, :progress, fn -> 99 end)
socket.assigns.progress
# => 42  (kept; the fun is not called)

assign/3 also takes a keyword list — assign(socket, progress: 42, error: nil) sets several keys at once, the form the Portal mount below uses.

mount, on the Portal

A LiveView for one enrollment loads its state in mount/3 through the Portal facade and nothing deeper. Portal.progress_of/1 takes the enrollment id and returns {:ok, 0..100} or a typed {:error, %Portal.Error{}}; each outcome maps to assigns. The one-time work — here a periodic refresh timer — is wrapped in connected?(socket), so it starts on the live mount and never on the disconnected first paint.

defmodule PortalWeb.EnrollmentLive do
  use PortalWeb, :live_view
  alias Portal.Error

  # mount/3 is the init/1 of a LiveView: set the socket's initial state
  @impl true
  def mount(%{"id" => enrollment_id}, _session, socket) do
    # one-time side effects belong behind connected?/1 — live mount only
    if connected?(socket), do: Process.send_after(self(), :refresh, 30_000)

    case Portal.progress_of(enrollment_id) do
      {:ok, percent} ->                  # percent :: 0..100
        {:ok, assign(socket, enrollment_id: enrollment_id, progress: percent, error: nil)}

      {:error, %Error{message: msg}} ->    # the closed error contract
        {:ok, assign(socket, enrollment_id: enrollment_id, progress: nil, error: msg)}
    end
  end
end

# iex> {:ok, socket} = EnrollmentLive.mount(%{"id" => "enr_42"}, %{}, socket)
# socket.assigns.progress
# => 42

The body of mount/3 names only Portal and %Portal.Error{} — the same facade a controller called in F6.01. The transport is a socket instead of a request; the boundary below it is the one F5 sealed.

Why it matters

Every later callback reads the state mount/3 set here. F6.06.2 turns a browser event into new assigns with handle_event/3; F6.06.3 renders those assigns and ships only the diff. A subscription opened under connected?/1 here is what the next module, F6.07 — PubSub, uses so a %Portal.Events.LearnerEnrolled{} broadcast can update many live views at once. An initial state set wrong here is inherited by every downstream callback, which is why mount is taken first.

F5 · GenServer init/1

init(arg) runs once and returns {:ok, state} — the process’s first state.

F6 · LiveView mount/3

mount(params, session, socket) returns {:ok, socket} — the same move, with a browser attached.

A LiveView is a GenServer with a browser on the other end. init/1 and mount/3 are the same idea: run once, return the initial state, then let messages drive the rest.

Recap

  • The socket is the LiveView’s process state; socket.assigns is the map the render reads.
  • mount/3 receives params, session, and socket, and returns {:ok, socket} with the initial assigns.
  • It runs twice: a disconnected HTTP render for the first paint, then a connected render over the socket.
  • connected?(socket) is false on the first run and true on the second — the guard for one-time side effects such as subscriptions and timers.
  • assign/3 sets keys unconditionally; assign_new/4 sets a key only when it is absent.

Next: handle_event & state — with the initial assigns in place, a browser event becomes a message that computes the next assigns. Back to the F6.06 overview.

References

The mount callback, the socket, and the assign helpers.