Phoenix LiveView
Elixir Phoenix Vinicius NegrisoloMy goal here:
Phoenix LiveView will turn 2 years soon and is still one of the best ways to develop a very interactive and modern Web App. Let’s navigate and discuss some topics about LiveView.
Phoenix LiveView
LiveView provides rich, real-time user experiences with server-rendered HTML
Creating a LiveView project
mix phx.new my_app --live
Our first LiveView
defmodule MyAppWeb.Router do
use MyAppWeb, :router
...
scope "/", MyAppWeb do
pipe_through :browser
live "/", HomeLive, :index
end
end
defmodule MyAppWeb.HomeLive do
use MyAppWeb, :live_view
@impl Phoenix.LiveView
def mount(_params, _session, socket) do
socket |> assign(whom: "world") |> ok()
end
defp ok(socket), do: {:ok, socket}
end
<h1>Hello <%= @whom %>! via <%= @live_action %></h1>
What is a LiveView again?
The LiveView
is a Controller / WebSocket
component. Both not either.
LiveView cycle
regular HTTP request ⇨
mount/3
⇨
handle_params/3
⇨
render/1
First Meaningful Paint
js kicks in ⇨
WebSocket connection ⇨
mount/3
⇨
handle_params/3
⇨
render/1
Redirect vs Patch
Phoenix
<%= link("home", to: "/") %>
redirect(conn, to: "/")
Phoenix LiveView
<%= live_redirect "home", to:
Routes.home_path(@socket, :index)
%>
<%= live_patch "home", to:
Routes.home_path(@socket, :index)
%>
{:noreply,
push_redirect(socket, to: "/")
}
{:noreply,
push_patch(socket, to: "/")
}
Redirect vs Patch
Redirect (another LiveView)
mount/3
⇨
handle_params/3
⇨
render/1
Patch (same LiveView)
handle_params/3
⇨
render/1
Let’s make it more React
<h1>Counted so far <%= @counter %></h1>
<button phx-click="count" phx-value-step="1">
Add 1 more
</button>
<button phx-click="count" phx-value-step="2">
Add 2 more
</button>
<button phx-click="count" phx-value-step="3">
Add 3 more
</button>
defmodule MyAppWeb.HomeLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
socket |> assign(counter: 0) |> ok()
end
def handle_event("count", %{"step" => step}, socket) do
counter = socket.assigns.counter + Integer.parse(step)
socket |> assign(counter: counter) |> noreply()
end
defp ok(socket), do: {:ok, socket}
defp noreply(socket), do: {:noreply, socket}
end
LiveView Event Handlers
- State (socket.assigns) lives in the backend 😀
- HTML diff goes through the wire ⚡️
- Re-render happens automatically (React) ⚛️
How to get Fresh Data?
Polling
Process.send_after(self(), :refresh, 1000)
⇨
defp handle_info(:refresh, socket) do
PubSub
MyAppWeb.Endpoint.broadcast(@topic, @event, payload)
MyAppWeb.Endpoint.subscribe(@topic)
⇨
def handle_info(%{topic: t, event: e, payload: p}, socket) do
Being Lazy and Polling to Refresh
defmodule MyAppWeb.HomeLive do
use MyAppWeb, :live_view
@refresh_interval 10000
def mount(_params, _session, socket) do
socket |> schedule_refresh() |> fetch_users() |> ok()
end
def handle_info(:refresh, socket) do
socket |> schedule_refresh() |> fetch_users() |> noreply()
end
defp schedule_refresh(socket) do
if connected?(socket) do
Process.send_after(self(), :refresh, @refresh_interval)
end
socket
end
defp fetch_users(socket) do
assign(socket, users: Accounts.get_users())
end
defp ok(socket), do: {:ok, socket}
defp noreply(socket), do: {:noreply, socket}
end
PubSub Not so Lazy Approach
defmodule MyAppWeb.HomeLive do
use MyAppWeb, :live_view
@topic "counter"
def mount(_params, _session, socket) do
if connected?(socket), do: MyAppWeb.Endpoint.subscribe(@topic)
socket |> fetch_users() |> ok()
end
def handle_event("count", %{"step" => step}, socket) do
counter = socket.assigns.counter + Integer.parse(step)
MyAppWeb.Endpoint.broadcast(@topic, "counter-update", counter)
socket |> assign(counter: counter) |> noreply()
end
def handle_info(%{topic: @topic, event: "counter-update", payload: counter}, socket) do
socket |> assign(counter: counter) |> noreply()
end
defp ok(socket), do: {:ok, socket}
defp noreply(socket), do: {:noreply, socket}
end
JS Integration
let liveSocket = new LiveSocket("/live", Socket, {
hooks: {InfiniteScroll},
params: {},
})
JS Integration => JS event to Elixir
js pushEvent
⇨
ex handle_event/3
<div id="infinite-scroll" phx-hook="InfiniteScroll" data-page="<%= @page %>">
</div>
const InfiniteScroll = {
page() { return this.el.dataset.page },
mounted(){
this.pending = this.page()
window.addEventListener("scroll", e => {
if(this.pending == this.page() && scrollAt() > 90){
this.pending = this.page() + 1
this.pushEvent("load-more", {})
}
})
},
updated(){ this.pending = this.page() }
}
JS Integration => Elixir event to JS
ex push_event
⇨
js handleEvent
<div id="chart" phx-hook="Chart">
{:noreply, push_event(socket, "points", %{points: new_points})}
const Chart = {
mounted(){
this.handleEvent("points", ({points}) => MyChartLib.addPoints(points))
}
}
LiveView Cycle
More topics, NO Time
- reuse code with
LiveComponent
- LiveComponent lifecycle
- connection errors handling
- more DOM bindings
- debounce / throttle
- more JS callbacks
- auth{entic,oriz}ation
- uploads
- DOM patching & Temporary Assigns
Summary
- Blazing Fast ⚡️
- State is on the Server
- Integrates well with JS ⚛️
- Regular CSS / SCSS
- Very well suited for web development!!!