In the modern web landscape, users expect instantaneous feedback. Whether it is a live chat, a real-time stock ticker, or a collaborative document editor, the “refresh button” is increasingly becoming a relic of the past. For years, achieving this level of interactivity required a complex “split-brain” architecture: a heavy JavaScript frontend (like React or Vue) communicating via APIs with a backend (like Node.js or Ruby on Rails).
This approach, while functional, introduces significant overhead. Developers have to manage state in two places, handle complex serialization/deserialization logic, and maintain two entirely different toolchains. Phoenix LiveView changes the game. It allows developers to build rich, interactive, real-time user experiences using server-rendered HTML, all within the Elixir ecosystem.
In this guide, we will dive deep into Phoenix LiveView. We will move from the foundational concepts of the BEAM (Erlang Virtual Machine) to building a fully functional real-time application. Whether you are a beginner or looking to sharpen your Elixir skills, this guide provides the technical depth and practical examples needed to master Phoenix LiveView.
Why Phoenix LiveView? The Problem with Modern SPAs
Before we jump into the code, let’s understand the “why.” Traditional Single Page Applications (SPAs) often suffer from “JavaScript fatigue.” To build a simple real-time feature, you often need:
- A REST or GraphQL API.
- State management libraries (Redux, Pinia, etc.).
- Client-side routing.
- Build tools like Webpack or Vite.
- Complex authentication persistence across two domains.
Phoenix LiveView eliminates this complexity by keeping the state on the server. When a user interacts with the page, a small message is sent over a persistent WebSocket (Phoenix Channel). The server processes the change, computes the difference in the HTML, and sends back only the “diff” to the client. The client-side LiveView library then patches the DOM in milliseconds. This results in apps that feel as snappy as React apps but are written entirely in Elixir.
Core Concepts: The Foundation of LiveView
To understand LiveView, you must understand three core pillars: Processes, WebSockets, and The Diffing Engine.
1. The BEAM and Processes
Unlike Ruby or Python, Elixir runs on the BEAM. In Elixir, every single user connection to a LiveView is its own isolated Process. These processes are incredibly lightweight—you can run hundreds of thousands of them on a single machine. If one user’s process crashes, it doesn’t affect anyone else. This “Share Nothing” architecture is what makes Phoenix so stable.
2. Persistent WebSockets
LiveView maintains a persistent connection via WebSockets. Instead of the overhead of a full HTTP request (headers, cookies, handshakes) for every click, LiveView sends tiny binary packets. This reduces latency significantly, especially on mobile networks.
3. The Diffing Engine
LiveView is smart. It doesn’t send the whole page back. It tracks which parts of your HTML are static and which are dynamic. When your state (assigns) changes, only the specific dynamic bits that changed are sent over the wire. This is why LiveView can often be faster and more bandwidth-efficient than a client-side framework fetching JSON.
Setting Up Your First Phoenix LiveView Project
To follow along, ensure you have Elixir and Phoenix installed. If you haven’t installed them yet, refer to the official Elixir installation guide. Let’s create a new project called reactive_app.
# Install the Phoenix project generator if you haven't
mix archive.install hex phx_new
# Create a new project with LiveView (LiveView is enabled by default in Phoenix 1.7+)
mix phx.new reactive_app
cd reactive_app
# Set up the database
mix setup
Now, let’s start the server to ensure everything is working:
iex -S mix phx.server
Navigate to localhost:4000. You should see the Phoenix welcome page.
The Lifecycle of a LiveView
A LiveView goes through a specific lifecycle. Understanding this is crucial for managing state and optimizing performance. The three most important callbacks are:
- mount/3: Invoked when the LiveView starts. This is where you initialize your data.
- handle_params/3: Invoked after mount and whenever the URL parameters change.
- render/1: This is where your HTML template is defined (usually in a
.heexfile).
Step-by-Step: Creating a Real-Time Counter
Let’s build a simple counter to see these callbacks in action. Create a new file at lib/reactive_app_web/live/counter_live.ex.
defmodule ReactiveAppWeb.CounterLive do
# Use the LiveView module functionality
use ReactiveAppWeb, :live_view
# 1. Mount: Initialize the state
# _params: URL parameters
# _session: Session data (like user_id)
# socket: The data structure representing the connection
def mount(_params, _session, socket) do
# Assign a starting value of 0 to the "val" key
{:ok, assign(socket, :val, 0)}
end
# 2. Render: Define the UI
# The ~H sigil is used for HEEx (HTML + Elixir Expressions) templates
def render(assigns) do
~H"""
<div class="p-10 text-center">
<div class="mt-4">
<%# phx-click sends an event to the server %>
<button phx-click="dec" class="px-4 py-2 bg-red-500 text-white rounded">-</button>
<button phx-click="inc" class="px-4 py-2 bg-green-500 text-white rounded">+</button>
</div>
</div>
"""
end
# 3. Handle Events: React to user input
# This function matches the "inc" event name from the button
def handle_event("inc", _params, socket) do
# Update the value in the socket assigns
{:noreply, update(socket, :val, &(&1 + 1))}
end
def handle_event("dec", _params, socket) do
{:noreply, update(socket, :val, &(&1 - 1))}
end
end
Now, add a route to your lib/reactive_app_web/router.ex file inside the browser scope:
scope "/", ReactiveAppWeb do
pipe_through :browser
live "/counter", CounterLive
end
Visit /counter in your browser. When you click the buttons, the count updates instantly. Notice how there is no full page reload. If you open your browser’s “Network” tab and look at the WS (WebSocket) section, you will see the tiny messages being sent back and forth.
Going Deeper: Real-Time Updates with Phoenix PubSub
The counter example is great, but it’s local to one user. What if we want everyone on the site to see the same counter update in real-time? For this, we use Phoenix PubSub (Publish/Subscribe).
PubSub allows different processes to communicate. When one user clicks “increment,” we will broadcast a message to a “topic.” All other users’ processes “subscribed” to that topic will receive the message and update their own state.
Updating the Counter to be Global
Modify the mount and handle_event functions in counter_live.ex:
@topic "global_counter"
def mount(_params, _session, socket) do
# Subscribe to the topic
if connected?(socket), do: Phoenix.PubSub.subscribe(ReactiveApp.PubSub, @topic)
{:ok, assign(socket, :val, 0)}
end
def handle_event("inc", _params, socket) do
new_val = socket.assigns.val + 1
# Broadcast the new value to everyone subscribed
Phoenix.PubSub.broadcast(ReactiveApp.PubSub, @topic, {:update_count, new_val})
{:noreply, assign(socket, :val, new_val)}
end
# We need a new handler for the PubSub message
def handle_info({:update_count, count}, socket) do
{:noreply, assign(socket, :val, count)}
end
Open two different browser windows side-by-side. Click the button in one, and watch the count update in both simultaneously! This is the power of Elixir’s concurrency model.
Phoenix LiveView Components
As your application grows, your templates will become cluttered. LiveView provides two types of components to keep code clean and reusable:
1. Function Components
These are pure functions that take a map of attributes and return HTML. They are ideal for reusable UI elements like buttons or input fields.
attr :type, :string, default: "button"
attr :rest, :global
slot :inner_block, required: true
def my_button(assigns) do
~H"""
<button type={@type} class="btn-primary" {@rest}>
<%= render_slot(@inner_block) %>
</button>
"""
end
2. LiveComponents
These are stateful components. They have their own mount, update, and handle_event callbacks. Use these when a component needs to handle its own internal state (like a complex search modal or an individual item in a dashboard).
Handling Forms and Validations
One of the best features of LiveView is real-time form validation. In traditional apps, you either wait for a submit button to see errors or write duplicate validation logic in JavaScript. In LiveView, you can run your backend Ecto validations as the user types.
def handle_event("validate", %{"user" => params}, socket) do
changeset =
%User{}
|> User.changeset(params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
By setting phx-change="validate" on your form tag, every keystroke sends the data to the server, runs your actual database schema validations, and returns error messages instantly. This ensures 100% consistency between your UI and your database rules.
Performance Optimization with Streams
What happens if you are building a social media feed with 10,000 posts? Keeping all those posts in the socket state would consume too much memory on the server. To solve this, Phoenix 1.7 introduced Streams.
Streams allow LiveView to manage large collections of data on the client side while keeping the server state minimal. Instead of storing the full list of items in socket.assigns, LiveView only sends the new or updated items, and the client-side JavaScript handles the DOM insertion/removal.
# In mount
def mount(_params, _session, socket) do
posts = Blog.list_posts()
{:ok, stream(socket, :posts, posts)}
end
# In template
<ul id="posts-list" phx-update="stream">
<li :for={{dom_id, post} <- @streams.posts} id={dom_id}>
<%= post.title %>
</li>
</ul>
Common Mistakes and How to Fix Them
1. Putting Too Much Data in Assigns
The Mistake: Storing large blobs of static data in the socket.
The Fix: Use temporary_assigns or Streams. Remember that everything in assigns is stored in memory for as long as the user is on the page. Only store dynamic data that changes frequently.
2. Blocking the LiveView Process
The Mistake: Performing a long-running API call or heavy calculation directly inside handle_event.
The Fix: Use Task.async or delegate the work to a background worker. If you block the LiveView process, the UI will become unresponsive for that specific user because the process cannot process the next “render” message until the task is finished.
3. Neglecting JavaScript Hooks
The Mistake: Trying to do *everything* in Elixir, even things like triggering a scroll-to-bottom or integrating a 3rd party chart library.
The Fix: Use LiveView Hooks. LiveView isn’t about *no* JavaScript; it’s about *less* JavaScript. Hooks allow you to run specific JS functions when an element is added to the DOM or when the server sends a specific event.
// assets/js/app.js
let Hooks = {}
Hooks.ScrollToBottom = {
updated() {
this.el.scrollTop = this.el.scrollHeight;
}
}
let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, ...})
Summary and Key Takeaways
Phoenix LiveView is a paradigm shift in web development. By leveraging the power of Elixir’s concurrency and the persistence of WebSockets, it allows you to build sophisticated real-time applications with a fraction of the code required by traditional SPA architectures.
- Simplicity: Write your frontend and backend in one language (Elixir).
- Reliability: Built on the BEAM, ensuring that user sessions are isolated and fault-tolerant.
- Performance: Only sends dynamic HTML diffs over the wire, resulting in low latency and low bandwidth usage.
- Productivity: Features like real-time form validation and PubSub are built-in and easy to implement.
Frequently Asked Questions (FAQ)
Is Phoenix LiveView SEO-friendly?
Yes. When a user or a search engine crawler first visits a LiveView URL, Phoenix performs a standard HTTP request and renders the full HTML on the server. This means Google and Bing can crawl your content just like a traditional static site. Only after the initial load does the page upgrade to a WebSocket connection.
Does LiveView replace React/Vue?
For many use cases, yes. It is excellent for CRUD apps, dashboards, real-time monitors, and forms. However, for applications requiring heavy client-side computation (like an offline-first image editor or a 3D game), a dedicated JavaScript framework may still be a better choice.
How does LiveView handle high latency or poor connections?
LiveView has built-in features to handle latency, such as “phx-disable-with” which can grey out buttons while a request is pending. It also automatically attempts to reconnect if the WebSocket drops, restoring the user’s state seamlessly when the connection returns.
Is LiveView difficult to scale?
Actually, LiveView scales remarkably well. Because it relies on Elixir processes, it can handle thousands of concurrent users on a single modest server. For multi-node scaling, Phoenix PubSub uses Distributed Erlang to sync messages across a cluster of servers automatically.
