LiveView
Phoenix LiveView Crash Course: Building Interactive Web Apps with Minimal JavaScript
Phoenix LiveView is a powerful technology for building interactive web applications with minimal JavaScript. This crash course will equip you with the core knowledge (85%) you need for daily use, while providing a foundation to explore the remaining advanced topics.
What is Phoenix LiveView?
Phoenix LiveView is a library for Phoenix applications that enables building interactive, real-time web interfaces without writing custom JavaScript. It uses a server-driven approach where the state is maintained on the server and only DOM differences are sent to the client via WebSockets.
Key Benefits:
- Server-Rendered: Maintains state on the server and pushes only HTML diffs to the client
- Simplified Programming: No need to maintain separate client and server implementations
- Real-Time Updates: WebSocket connection enables instant UI updates
- Minimal JavaScript: Build interactive apps with little to no custom JS
- Scalable: Built on Elixir’s lightweight process model for excellent performance
Prerequisites
Before getting started with Phoenix LiveView, you’ll need:
- Elixir and Erlang: Install from the Elixir website
- Phoenix Framework: Basic familiarity with Phoenix is helpful
Installation and Setup
Let’s set up a new Phoenix project with LiveView:
# Install Phoenix
mix archive.install hex phx_new
# Create a new Phoenix project with LiveView
mix phx.new my_app --live
# Navigate to the project directory
cd my_app
# Set up the database
mix ecto.setup
# Start the Phoenix server
mix phx.server
Your application will be available at http://localhost:4000
.
Core Concepts
LiveView Lifecycle
graph TD A[Browser Request] --> B[Mount] B --> C[Connected?] C -->|Yes| D[WebSocket Connection] C -->|No| E[Static HTML Render] D --> F[Client Connected] F --> G[Handle Events] G --> H[Update State] H --> I[Re-render] I --> J[Send DOM Diffs] J --> G
1. Mount Function
The entry point of a LiveView. It initializes the state and is called when the LiveView is first rendered:
def mount(_params, _session, socket) do
# Initial state setup
products = MyApp.Catalog.list_products()
# Return the socket with assigned values
{:ok, assign(socket, products: products, cart_count: 0)}
end
2. Render Function
Defines the HTML structure of your LiveView. Called whenever the state changes:
def render(assigns) do
~H"""
<div class="container">
<h1>Products</h1>
<div class="products-list">
<%= for product <- @products do %>
<div class="product-card">
<h3><%= product.name %></h3>
<p><%= product.description %></p>
<span class="price">$<%= product.price %></span>
<button phx-click="add_to_cart" phx-value-id={product.id}>
Add to Cart
</button>
</div>
<% end %>
</div>
<div class="cart-summary">
Items in cart: <%= @cart_count %>
</div>
</div>
"""
end
3. Event Handling
When a user interacts with the page, events are sent to the server:
def handle_event("add_to_cart", %{"id" => product_id}, socket) do
# Convert product_id to integer if needed
product_id = String.to_integer(product_id)
# Add logic to update cart in the database if needed
# ...
# Update the socket state
{:noreply, update(socket, :cart_count, &(&1 + 1))}
end
Building a Simple Counter App
Let’s build a basic counter application to demonstrate LiveView in action:
# In lib/my_app_web/live/counter_live.ex
defmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
# Initialize the state
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
# Define the HTML structure
def render(assigns) do
~H"""
<div class="counter-container">
<h1>Counter: <%= @count %></h1>
<button phx-click="increment">+</button>
<button phx-click="decrement">-</button>
<button phx-click="reset">Reset</button>
</div>
"""
end
# Handle increment event
def handle_event("increment", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
# Handle decrement event
def handle_event("decrement", _params, socket) do
{:noreply, update(socket, :count, &(&1 - 1))}
end
# Handle reset event
def handle_event("reset", _params, socket) do
{:noreply, assign(socket, count: 0)}
end
end
Now, add this LiveView to your router:
# In lib/my_app_web/router.ex
scope "/", MyAppWeb do
pipe_through :browser
live "/counter", CounterLive
end
Understanding Process Communication
LiveView leverages Elixir’s lightweight processes for handling state and communication:
graph TD A[Browser] --> B[Phoenix Endpoint] B --> C[LiveView Process] C --> D[GenServer] C --> E[PubSub] E --> F[Other LiveViews] D --> G[Database]
PubSub for Real-Time Updates
# Subscribe to a topic when the LiveView mounts
def mount(_params, _session, socket) do
if connected?(socket) do
# Only subscribe when the WebSocket is connected
Phoenix.PubSub.subscribe(MyApp.PubSub, "product:updates")
end
{:ok, assign(socket, products: MyApp.Catalog.list_products())}
end
# Handle messages from PubSub
def handle_info({:product_updated, product}, socket) do
# Update the products list when a product changes
updated_products = Enum.map(socket.assigns.products, fn p ->
if p.id == product.id, do: product, else: p
end)
{:noreply, assign(socket, products: updated_products)}
end
Form Handling and Validation
LiveView excels at handling forms with real-time validation:
def render(assigns) do
~H"""
<div>
<h1>New Product</h1>
<.form
:let={f}
for={@changeset}
phx-change="validate"
phx-submit="save">
<div class="form-group">
<%= label f, :name %>
<%= text_input f, :name %>
<%= error_tag f, :name %>
</div>
<div class="form-group">
<%= label f, :price %>
<%= number_input f, :price, step: "0.01" %>
<%= error_tag f, :price %>
</div>
<div class="form-group">
<%= label f, :description %>
<%= textarea f, :description %>
<%= error_tag f, :description %>
</div>
<div class="form-actions">
<%= submit "Save", class: "btn btn-primary" %>
</div>
</.form>
</div>
"""
end
# Initialize the form changeset
def mount(_params, _session, socket) do
changeset = MyApp.Catalog.change_product(%MyApp.Catalog.Product{})
{:ok, assign(socket, changeset: changeset)}
end
# Validate the form in real-time as the user types
def handle_event("validate", %{"product" => product_params}, socket) do
changeset =
%MyApp.Catalog.Product{}
|> MyApp.Catalog.change_product(product_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, changeset: changeset)}
end
# Handle form submission
def handle_event("save", %{"product" => product_params}, socket) do
case MyApp.Catalog.create_product(product_params) do
{:ok, _product} ->
{:noreply,
socket
|> put_flash(:info, "Product created successfully")
|> push_redirect(to: Routes.product_index_path(socket, :index))}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
LiveView Components
Components help you break down complex UIs into reusable pieces:
graph TD A[Parent LiveView] --> B[Component 1] A --> C[Component 2] B --> D[Nested Component]
Creating a Component
defmodule MyAppWeb.ProductComponent do
use Phoenix.Component
# Define the component function
def product(assigns) do
~H"""
<div class="product-card" id={"product-#{@product.id}"}>
<h3><%= @product.name %></h3>
<p><%= @product.description %></p>
<span class="price">$<%= @product.price %></span>
<button phx-click="add_to_cart" phx-value-id={@product.id} phx-target={@myself}>
Add to Cart
</button>
<div class="likes">Likes: <%= @likes %></div>
<button phx-click="like" phx-target={@myself}>Like</button>
</div>
"""
end
# Handle component events
def handle_event("like", _params, socket) do
{:noreply, update(socket, :likes, &(&1 + 1))}
end
def handle_event("add_to_cart", _params, socket) do
# Send message to parent LiveView
send(self(), {:add_to_cart, socket.assigns.product})
{:noreply, socket}
end
end
Using Components in a LiveView
def render(assigns) do
~H"""
<div class="container">
<h1>Products</h1>
<div class="products-list">
<%= for product <- @products do %>
<.live_component
module={MyAppWeb.ProductComponent}
id={"product-#{product.id}"}
product={product}
likes={0}
/>
<% end %>
</div>
<div class="cart-summary">
Items in cart: <%= @cart_count %>
</div>
</div>
"""
end
Authentication
Implementing authentication in LiveView:
# In router.ex
pipeline :auth do
plug :put_root_layout, {MyAppWeb.Layouts, :auth}
plug :fetch_current_user
end
live_session :authenticated, on_mount: {MyAppWeb.LiveAuth, :ensure_authenticated} do
scope "/", MyAppWeb do
pipe_through [:browser, :auth]
live "/dashboard", DashboardLive
live "/profile", ProfileLive
end
end
# In live_auth.ex
defmodule MyAppWeb.LiveAuth do
import Phoenix.LiveView
def on_mount(:ensure_authenticated, _params, session, socket) do
case session do
%{"user_id" => user_id} ->
user = MyApp.Accounts.get_user!(user_id)
{:cont, assign(socket, current_user: user)}
_ ->
{:halt, redirect(socket, to: "/login")}
end
end
end
Navigation
You can use live_patch
and live_redirect
for navigation:
def render(assigns) do
~H"""
<nav>
<.link patch={~p"/products"}>All Products</.link>
<.link patch={~p"/products/new"}>New Product</.link>
</nav>
<div>
<%= if @live_action == :index do %>
<h2>Product Listing</h2>
<!-- Product listing content -->
<% else %>
<h2>New Product</h2>
<!-- New product form -->
<% end %>
</div>
"""
end
def handle_params(_params, _uri, socket) do
{:noreply, socket}
end
The Remaining 15%: Advanced Topics
As you continue your journey with Phoenix LiveView, here are the advanced topics (the remaining 15%) to explore:
JavaScript Interoperability:
- Use JS hooks for integrating with third-party libraries
- Custom client-side event handling
- Advanced DOM manipulations
LiveView Testing:
- Testing LiveView components
- Testing real-time functionality
- Integration tests with authentication
Performance Optimization:
- Using temporary assigns for large lists
- Implementing pagination
- Stream for efficient list updates
- Database query optimization
File Upload Handling:
- Managing file uploads
- Progress tracking
- Multiple file uploads
- Image previews
Advanced State Management:
- Using GenServers for stateful applications
- Implementing complex state machines
- Event sourcing patterns
Deployment and Scaling:
- Production environment setup
- Load balancing considerations
- Clustering Phoenix nodes
LiveView Hooks:
- Advanced usage of hooks
- Lifecycle management
- DOM operations
Summary
In this crash course, we’ve covered the core 85% of Phoenix LiveView that you’ll need for daily use:
- Installation and setup
- LiveView lifecycle (mount, render, events)
- State management and updates
- Real-time communication with PubSub
- Form handling and validation
- Components and composition
- Authentication and navigation
Phoenix LiveView offers a powerful approach to building interactive web applications with minimal JavaScript. By leveraging Elixir’s concurrency model and OTP principles, LiveView provides excellent performance and developer experience.
The remaining 15% (advanced topics) will become relevant as your applications grow in complexity. With the foundation from this crash course, you’ll be well-equipped to explore those areas as needed.
Remember that LiveView is built on top of Phoenix and Elixir, so deepening your knowledge of those technologies will further enhance your LiveView skills.