The big win for me has been the built-in PubSub primitives plus LiveView. Since the backend is already maintaining a WebSocket connection with every client, it's trivial to push updates.
Here is an example. Imagine something like a multiplayer Google Forms editor that renders a list of drag-droppable cards. Below is a complete LiveView module that renders the cards, and subscribes to "card was deleted" and "cards were reordered" events.
```
defmodule MyApp.ProjectLive.Edit do
use MyApp, :live_view
import MyApp.Components.Editor.Card
def mount(%{"project_id" => id}, _session, socket) do
# Subscribe view to project events
Phoenix.PubSub.subscribe(MyApp.PubSub, "project:#{id}")
project = MyApp.Projects.get_project(id)
socket =
socket
|> assign(:project, project)
|> assign(:cards_drag_handle_class, "CARD_DRAG_HANDLE")
{:ok, socket}
end
def handle_info({:cards, :deleted, card_id}, socket) do
# handle project events matching signature: `{:cards, :deleted, payload}`
cards = Enum.reject(socket.assigns.project.cards, fn card -> card.id == card_id end)
project = %{socket.assigns.project | cards: cards}
socket = assign(socket, :project, project)
# LiveView will diff and re-render automatically
{:noreply, socket}
end
def handle_info({:cards, :reordered, card_change_list}, socket) do
# omitted for brevity, same concept as above
{:noreply, socket}
end
def render(assigns) do
~H"""
<div>
<h1>{@project.name}</h1>
<div
id="cards-drag-manager"
phx-hook="DragDropMulti"
data-handle-class-name={@cards_drag_handle_class}
data-drop-event-name="reorder_cards"
data-container-ids="cards-container"
/>
<div class="space-y-4" id="cards-container">
<.card
:for={card <- @project.cards}
card={card}
cards_drag_handle_class={@cards_drag_handle_class}
/>
</div>
</div>
"""
end
end
```
What would this take in a React SPA? Well of course there are tons of great tools out there, like Cloud Firestore, Supabase Realtime, etc. But my app is just a vanilla postgres + phoenix monolith! And it's so much easier to test. Again, just using the built-in testing libraries.
For rich drag-drop (with drop shadows, auto-scroll, etc.) I inlined DragulaJS[1] which is ~1000 lines of vanilla .js. As a React dev I might have been tempted to `npm install` something like `react-beautiful-dnd`, which is 6-10x larger, (and is, I just learned, now deprecated by the maintainers!!)
The important question is, what have I sacrificed? The primary tradeoff is that the 'read your own writes' experience can feel sluggish if you are used to optimistic UI via React setState(). This is a hard one to stomach as a react dev. But Phoenix comes with GitHub-style viewport loading bars which is enough user enough feedback to be passable.
p.s. guess what Supabase Realtime is using under the hood[2] ;-)
[1] https://bevacqua.github.io/dragula/
[2] https://supabase.com/docs/guides/realtime/architecture