Live view and handle info
Phoenix LiveView handle_info Guide for Web Developers
What is handle_info?
handle_info is Phoenix LiveView's way of handling messages sent to the LiveView process from outside normal user interactions (like clicks). It's your LiveView's inbox for asynchronous events.
In traditional web development, everything is request-response: user clicks, server responds, done. But LiveView processes are long-running and can receive messages from other parts of your system while they're alive.
Basic Syntax
def handle_info(message, socket) do
# Process the message
# Update socket state if needed
{:noreply, socket}
end
Example 1: Timer (sending to self)
The simplest way to trigger handle_info - send a message to yourself:
defmodule AppWeb.PageLive do
use AppWeb, :live_view
def mount(_params, _session, socket) do
Process.send_after(self(), :refresh, 5000)
{:ok, socket}
end
def handle_info(:refresh, socket) do
IO.inspect("Message received after 5 seconds!")
{:noreply, socket}
end
def render(assigns) do
~H"""
Page
"""
end
end
Example 2: Sending Between LiveViews with PubSub
This is the recommended way to send messages between LiveViews.
In PageLive (the receiver):
defmodule AppWeb.PageLive do
use AppWeb, :live_view
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(App.PubSub, "events")
end
{:ok, socket}
end
def handle_info(:my_message, socket) do
IO.inspect("handle_info received!")
{:noreply, socket}
end
def render(assigns) do
~H"""
Page
"""
end
end
In HomeLive (the sender):
defmodule AppWeb.HomeLive do
use AppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, socket}
end
def handle_event("trigger_page", _params, socket) do
Phoenix.PubSub.broadcast(App.PubSub, "events", :my_message)
{:noreply, socket}
end
def render(assigns) do
~H"""
<button phx-click="trigger_page">Trigger Page handle_info</button>
"""
end
end
Why do you need both subscribe AND broadcast?
- Subscribe = "I want to listen to messages on the 'events' topic"
- Broadcast = "Send this message to everyone listening to 'events'"
Without subscribe, the LiveView won't receive broadcasts. Think of it like tuning a radio to a channel - you must tune in (subscribe) before you can hear what's being transmitted (broadcast).
PubSub works with multiple LiveViews
If you have 3 PageLive tabs open, all 3 will receive the broadcast. That's a feature, not a bug.
Example 3: Process Registration (not recommended)
You can register a LiveView process with a name and send directly to it:
In PageLive:
defmodule AppWeb.PageLive do
use AppWeb, :live_view
def mount(_params, _session, socket) do
if connected?(socket) do
Process.register(self(), :page_live_process)
end
{:ok, socket}
end
def handle_info(:my_message, socket) do
IO.inspect("handle_info received!")
{:noreply, socket}
end
def render(assigns) do
~H"""
Page
"""
end
end
In HomeLive:
defmodule AppWeb.HomeLive do
use AppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, socket}
end
def handle_event("send_to_page", _params, socket) do
send(:page_live_process, :my_message)
{:noreply, socket}
end
def render(assigns) do
~H"""
<button phx-click="send_to_page">Send to Page</button>
"""
end
end
Problems with this approach:
- Only works with ONE PageLive instance at a time
- Crashes if name is already taken
- Requires error handling bloat
- PageLive must be mounted first
Use PubSub instead - it's simpler and more robust.
Common Sources of Messages
handle_info receives messages from:
- Timers:
Process.send_after(self(), :tick, 1000) - PubSub broadcasts:
Phoenix.PubSub.broadcast(App.PubSub, "topic", :msg) - Other processes:
send(liveview_pid, :msg) - Tasks: Background jobs completing
- GenServers: Other parts of your application
Safety: Catch-all handle_info
Always add a catch-all to prevent crashes from unexpected messages:
def handle_info(msg, socket) do
IO.warn("Unhandled message: #{inspect(msg)}")
{:noreply, socket}
end
Key Takeaways
- handle_info handles asynchronous messages sent to your LiveView
- Use
send(self(), :msg)to send messages to yourself - Use PubSub (not process registration) to send between LiveViews
- You must subscribe to a topic before you can receive broadcasts on that topic
- Always include a catch-all handle_info to handle unexpected messages
_____________________________________
Building a Real-Time Phoenix LiveView App with GenServer and PubSub
This tutorial demonstrates how to create a Phoenix LiveView application that uses:
- A GenServer to send periodic timer updates
- Phoenix PubSub for broadcasting messages
- LiveView's
handle_info/2to receive and handle messages - Multiple LiveViews communicating with each other
What We're Building
- TimerServer: A GenServer that broadcasts a message every 3 seconds
- PageLive: A LiveView that subscribes to timer updates and displays them
- HomeLive: A LiveView with a button that can manually trigger messages to PageLive
Step 1: Create the GenServer
Create lib/app/timer_server.ex:
defmodule App.TimerServer do
use GenServer
require Logger
@interval 3_000 # 3 seconds
# Client API
def start_link(_opts) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
# Server Callbacks
@impl true
def init(state) do
# Start the timer immediately
schedule_tick()
{:ok, state}
end
@impl true
def handle_info(:tick, state) do
# Log to console
message = "Timer tick at #{Time.utc_now()}"
IO.puts(message)
Logger.info(message)
# Broadcast to all subscribed LiveViews via PubSub
Phoenix.PubSub.broadcast(App.PubSub, "events", {:timer_update, message})
# Schedule the next tick
schedule_tick()
{:noreply, state}
end
# Private Functions
defp schedule_tick do
Process.send_after(self(), :tick, @interval)
end
end
Key Concepts:
GenServer Basics:
use GenServer- Brings in GenServer behaviourstart_link/1- Called when the GenServer startsinit/1- Initializes state and starts the timer
Timer Pattern:
Process.send_after(self(), :tick, @interval)- Schedules a message to ourselveshandle_info(:tick, state)- Receives the :tick message and processes it- We call
schedule_tick()again to create a recurring timer
Broadcasting:
Phoenix.PubSub.broadcast(App.PubSub, "events", {:timer_update, message})App.PubSub- The PubSub name (configured in application.ex)"events"- The topic name (subscribers must use the same topic){:timer_update, message}- The message payload (a tuple with an atom tag)
Step 2: Register the GenServer in the Supervision Tree
Edit lib/app/application.ex:
defmodule App.Application do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
AppWeb.Telemetry,
App.Repo,
{DNSCluster, query: Application.get_env(:app, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: App.PubSub},
# Start the timer server
App.TimerServer, # <- Add this line
# Start to serve requests, typically the last entry
AppWeb.Endpoint
]
opts = [strategy: :one_for_one, name: App.Supervisor]
Supervisor.start_link(children, opts)
end
@impl true
def config_change(changed, _new, removed) do
AppWeb.Endpoint.config_change(changed, removed)
:ok
end
end
Key Concepts:
Supervision Tree:
- The
childrenlist defines all processes that should start with the app - The supervisor automatically starts, monitors, and restarts these processes if they crash
App.TimerServeruses the default child_spec (provided byuse GenServer)- The supervisor will call
App.TimerServer.start_link/1at startup
Supervisor Strategy:
:one_for_one- If a child crashes, only that child is restarted (not siblings)
Step 3: Create PageLive (Subscribes to Timer Updates)
Create lib/app_web/live/page_live.ex:
defmodule AppWeb.PageLive do
use AppWeb, :live_view
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(App.PubSub, "events")
end
{:ok, assign(socket, :last_message, "Waiting for timer...")}
end
def handle_info(:my_message, socket) do
IO.inspect("handle_info received")
{:noreply, socket}
end
def handle_info({:timer_update, message}, socket) do
IO.puts("PageLive received: #{message}")
{:noreply, assign(socket, :last_message, message)}
end
def render(assigns) do
~H"""
<div class="p-8">
<h1 class="text-2xl font-bold mb-4">Timer Demo</h1>
<p class="text-gray-700">Last message: <%= @last_message %></p>
</div>
"""
end
end
Key Concepts:
mount/3 Lifecycle:
- Called when the LiveView initializes
connected?(socket)- Returns true only after the WebSocket connection is established- We only subscribe after connection to avoid subscribing during the initial static HTML render
PubSub Subscribe:
Phoenix.PubSub.subscribe(App.PubSub, "events")- Subscribes this LiveView process to the "events" topic- Any messages broadcast to "events" will be sent to this process
handle_info/2:
- Receives messages sent directly to the LiveView process
- Pattern matches on the message structure
{:timer_update, message}- Matches messages from TimerServer:my_message- Matches messages from HomeLive (see next step)
Socket Assigns:
assign(socket, :last_message, message)- Updates the socket state- When assigns change, LiveView automatically re-renders the component
- Access in template with
@last_message
Step 4: Create HomeLive (Triggers Manual Messages)
Create lib/app_web/live/home_live.ex:
defmodule AppWeb.HomeLive do
use AppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, socket}
end
def handle_event("trigger_page", _params, socket) do
Phoenix.PubSub.broadcast(App.PubSub, "events", :my_message)
{:noreply, socket}
end
def render(assigns) do
~H"""
<button phx-click="trigger_page">Trigger Page handle_info</button>
"""
end
end
Key Concepts:
handle_event/3:
- Receives events from the client (user interactions)
"trigger_page"- Matches thephx-clickattribute value in the button- Broadcasts
:my_messageto the "events" topic
phx-click Binding:
phx-click="trigger_page"- Sends a "trigger_page" event to the server when clicked- The LiveView automatically handles the client-server communication
Cross-LiveView Communication:
- HomeLive broadcasts a message via PubSub
- PageLive receives it in
handle_info(:my_message, socket) - This demonstrates how separate LiveView processes can communicate
How It All Works Together
The Flow:
- Application Starts:
- Supervisor starts all children including
App.TimerServer - TimerServer's
init/1schedules the first tick
- Supervisor starts all children including
- User Visits PageLive:
mount/3is called- After WebSocket connects, subscribes to "events" topic
- Initial state shows "Waiting for timer..."
- Every 3 Seconds:
- TimerServer receives
:tickviahandle_info - Broadcasts
{:timer_update, message}to "events" topic - PageLive receives the message in its
handle_info - Updates
@last_messageassign - LiveView automatically re-renders with new message
- TimerServer receives
- User Clicks Button in HomeLive:
- Browser sends "trigger_page" event to server
handle_event("trigger_page", ...)broadcasts:my_message- PageLive's
handle_info(:my_message, ...)receives it - Logs to console
Message Flow Diagram:
┌─────────────────┐
│ TimerServer │
│ │
│ Every 3s: │
│ broadcast() │
└────────┬────────┘
│
├─────────────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ PageLive │ │ HomeLive │
│ (subscribed) │ │ (not sub'd) │
│ │ │ │
│ handle_info │ │ Button click │
│ updates UI │◄──┤ broadcast() │
└─────────────────┘ └─────────────────┘
Key Takeaways
When to Use GenServer:
- Background tasks that need to run continuously
- Maintaining state across requests
- Scheduled/periodic operations
- As a single source of truth for application state
When to Use PubSub:
- Broadcasting to multiple subscribers
- Decoupling components (sender doesn't know about receivers)
- Real-time updates to LiveViews
- Cross-process communication
When to Use handle_info:
- Receiving PubSub messages in LiveView
- Receiving messages from GenServers
- Receiving Process messages (like our :tick)
- Any asynchronous message delivery
LiveView Mount Connected Check:
if connected?(socket) do Phoenix.PubSub.subscribe(App.PubSub, "events") end
This prevents subscribing during initial static render, only subscribing once the WebSocket connects.
Running the App
- Start the Phoenix server:
mix phx.server
- Visit http://localhost:4000 to see PageLive
- Should see timer updates every 3 seconds
- Open HomeLive in another tab
- Click the button to trigger a manual message to PageLive
- Check the terminal logs
- See TimerServer broadcasting messages
- See PageLive receiving messages
Extending This Pattern
You can extend this pattern for:
- Real-time dashboards
- Chat applications
- Notifications systems
- Live data feeds
- Multiplayer games
- Collaborative editing tools
The combination of GenServer for state management and PubSub for broadcasting makes Phoenix incredibly powerful for real-time applications.