jonnifyelixir
F6 / F6.07 / subscribe

F6.07 · part 2 of 3

Subscribing a LiveView

A LiveView receives broadcasts the same way a GenServer receives any message: in handle_info/2. The two halves are subscribe and handle. You subscribe on the connected mount — behind the connected?/1 guard from F6.06.1, so the throwaway first paint does not join — with one call to the facade’s Portal.subscribe("courses"). From then on, any message broadcast on that topic lands in the LiveView process’s mailbox and is delivered to handle_info/2, which pattern-matches the event tuple, updates the socket assigns, and returns {:noreply, socket} — triggering the usual re-render and diff. This is the distinction worth holding onto: handle_event/3 handles messages from the browser (a click, a keystroke), while handle_info/2 handles messages from other processes (a PubSub broadcast, a timer, a task result). Both end the same way — new assigns, a re-render — but they are different doorways into the LiveView, and keeping them straight is most of understanding real-time updates. Paired with streams from F6.06.3, a single broadcast inserts one row into every connected client.

Four parts: subscribe versus the two handlers, the path a broadcast takes into the LiveView, the mount-and-handle code, and driving a stream from a broadcast.

Two doorways

A LiveView reacts to two kinds of message. subscribe opts it into a topic. handle_info/2 receives process messages — PubSub broadcasts, timers. handle_event/3 receives browser events — clicks, input. Select one.

Subscribe and handlers · select one

subscribe Portal.subscribe("courses") — join on the connected mount opt in handle_info/2 handle_info({event, course}, socket) — a process message from PubSub handle_event/3 handle_event("create", params, socket) — a browser event from the page

callback: subscribe

handles: join a topic on connected mount

Mixing the two up is the classic beginner bug: trying to catch a broadcast in handle_event, or a click in handle_info. The cure is the one question — did this come from a browser or a process?

A broadcast arrives

The broadcast from F6.07.1 is sent on "courses". Because this LiveView subscribed, the message is placed in its mailbox and the runtime calls handle_info/2 with it. The handler updates assigns and returns {:noreply, socket}; render runs; the diff goes to this client’s browser. The same arc as a browser event, entered through the other door.

PubSub → mailbox → handle_info → diff

PubSub "courses" message MAILBOX this LiveView handle_info/2 match event, update assigns {:noreply, socket} render → diff to this browser a browser click instead enters at handle_event/3 both paths end in a re-render

Nothing in the broadcast knows or cares which LiveViews are listening. The publisher fires once; the runtime fans the message out to every subscribed mailbox. Adding a tenth viewer changes nothing in the publishing code.

Subscribe and handle

mount/3 subscribes once, on the connected pass. handle_info/2 matches {:course_created, course} and prepends it to @courses with update/3. A new course created anywhere — by this user or another — now appears at the top of this list without a reload.

def mount(_params, _session, socket) do
  if connected?(socket), do: Portal.subscribe("courses")
  {:ok, assign(socket, courses: Portal.list_courses())}
end

# a broadcast arrives as a process message, not a browser event
def handle_info({:course_created, course}, socket) do
  {:noreply, update(socket, :courses, fn courses -> [course | courses] end)}
end

update/3 transforms one assign from its current value, which is cleaner than reading and re-assigning. It is also exactly how you would evolve a GenServer’s state — the LiveView is that pattern with a screen attached.

Updating a stream

With the stream from F6.06.3, the broadcast updates the DOM directly instead of a list in memory. stream_insert/3 with at: 0 prepends the new row; a separate :course_updated clause replaces a row in place by id. Every connected client runs the same handler, so the feed updates for all of them at once.

# with streams (F6.06.3): insert one row in every client's DOM
def handle_info({:course_created, course}, socket) do
  {:noreply, stream_insert(socket, :courses, course, at: 0)}
end

def handle_info({:course_updated, course}, socket) do
  {:noreply, stream_insert(socket, :courses, course)}   # replaces the row by id
end

subscribe once

On the connected mount, behind connected?/1.

handle every event

handle_info/2 updates a stream; all clients update together.

Next: channels & presence — the primitive beneath LiveView, and tracking who is on a topic so you can show a live viewer count.