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, andsocket, and returns{:ok, socket}with the initial assigns set. - connected?/1
- A predicate on the socket:
falseduring the disconnected first paint,trueonce 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
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
socketis the LiveView’s process state;socket.assignsis the map the render reads. mount/3receivesparams,session, andsocket, 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)isfalseon the first run andtrueon the second — the guard for one-time side effects such as subscriptions and timers.assign/3sets keys unconditionally;assign_new/4sets 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.
Sources
mount/3— Phoenix.LiveView — params, session, socket, and the two-stage connect.connected?/1— Phoenix.LiveView — guarding one-time side effects.assign/3&assign_new/4— Phoenix.Component — writing the socket assigns.