Live view and handle info: Difference between revisions
(Created page with "= 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 syst...") |
No edit summary |
||
| Line 197: | Line 197: | ||
* You must '''subscribe''' to a topic before you can receive broadcasts on that topic | * 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 | * 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 <code>handle_info/2</code> to 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 <code>lib/app/timer_server.ex</code>: | |||
<syntaxhighlight lang="elixir"> | |||
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 | |||
</syntaxhighlight> | |||
=== Key Concepts: === | |||
'''GenServer Basics''': | |||
* <code>use GenServer</code> - Brings in GenServer behaviour | |||
* <code>start_link/1</code> - Called when the GenServer starts | |||
* <code>init/1</code> - Initializes state and starts the timer | |||
'''Timer Pattern''': | |||
* <code>Process.send_after(self(), :tick, @interval)</code> - Schedules a message to ourselves | |||
* <code>handle_info(:tick, state)</code> - Receives the :tick message and processes it | |||
* We call <code>schedule_tick()</code> again to create a recurring timer | |||
'''Broadcasting''': | |||
* <code>Phoenix.PubSub.broadcast(App.PubSub, "events", {:timer_update, message})</code> | |||
** <code>App.PubSub</code> - The PubSub name (configured in application.ex) | |||
** <code>"events"</code> - The topic name (subscribers must use the same topic) | |||
** <code>{:timer_update, message}</code> - The message payload (a tuple with an atom tag) | |||
== Step 2: Register the GenServer in the Supervision Tree == | |||
Edit <code>lib/app/application.ex</code>: | |||
<syntaxhighlight lang="elixir"> | |||
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 | |||
</syntaxhighlight> | |||
=== Key Concepts: === | |||
'''Supervision Tree''': | |||
* The <code>children</code> list defines all processes that should start with the app | |||
* The supervisor automatically starts, monitors, and restarts these processes if they crash | |||
* <code>App.TimerServer</code> uses the default child_spec (provided by <code>use GenServer</code>) | |||
* The supervisor will call <code>App.TimerServer.start_link/1</code> at startup | |||
'''Supervisor Strategy''': | |||
* <code>:one_for_one</code> - If a child crashes, only that child is restarted (not siblings) | |||
== Step 3: Create PageLive (Subscribes to Timer Updates) == | |||
Create <code>lib/app_web/live/page_live.ex</code>: | |||
<syntaxhighlight lang="elixir"> | |||
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 | |||
</syntaxhighlight> | |||
=== Key Concepts: === | |||
'''mount/3 Lifecycle''': | |||
* Called when the LiveView initializes | |||
* <code>connected?(socket)</code> - 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''': | |||
* <code>Phoenix.PubSub.subscribe(App.PubSub, "events")</code> - 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 | |||
* <code>{:timer_update, message}</code> - Matches messages from TimerServer | |||
* <code>:my_message</code> - Matches messages from HomeLive (see next step) | |||
'''Socket Assigns''': | |||
* <code>assign(socket, :last_message, message)</code> - Updates the socket state | |||
* When assigns change, LiveView automatically re-renders the component | |||
* Access in template with <code>@last_message</code> | |||
== Step 4: Create HomeLive (Triggers Manual Messages) == | |||
Create <code>lib/app_web/live/home_live.ex</code>: | |||
<syntaxhighlight lang="elixir"> | |||
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 | |||
</syntaxhighlight> | |||
=== Key Concepts: === | |||
'''handle_event/3''': | |||
* Receives events from the client (user interactions) | |||
* <code>"trigger_page"</code> - Matches the <code>phx-click</code> attribute value in the button | |||
* Broadcasts <code>:my_message</code> to the "events" topic | |||
'''phx-click Binding''': | |||
* <code>phx-click="trigger_page"</code> - 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 <code>handle_info(:my_message, socket)</code> | |||
* This demonstrates how separate LiveView processes can communicate | |||
== How It All Works Together == | |||
=== The Flow: === | |||
# '''Application Starts''': | |||
#* Supervisor starts all children including <code>App.TimerServer</code> | |||
#* TimerServer's <code>init/1</code> schedules the first tick | |||
# '''User Visits PageLive''': | |||
#* <code>mount/3</code> is called | |||
#* After WebSocket connects, subscribes to "events" topic | |||
#* Initial state shows "Waiting for timer..." | |||
# '''Every 3 Seconds''': | |||
#* TimerServer receives <code>:tick</code> via <code>handle_info</code> | |||
#* Broadcasts <code>{:timer_update, message}</code> to "events" topic | |||
#* PageLive receives the message in its <code>handle_info</code> | |||
#* Updates <code>@last_message</code> assign | |||
#* LiveView automatically re-renders with new message | |||
# '''User Clicks Button in HomeLive''': | |||
#* Browser sends "trigger_page" event to server | |||
#* <code>handle_event("trigger_page", ...)</code> broadcasts <code>:my_message</code> | |||
#* PageLive's <code>handle_info(:my_message, ...)</code> receives it | |||
#* Logs to console | |||
=== Message Flow Diagram: === | |||
<pre> | |||
┌─────────────────┐ | |||
│ TimerServer │ | |||
│ │ | |||
│ Every 3s: │ | |||
│ broadcast() │ | |||
└────────┬────────┘ | |||
│ | |||
├─────────────────────┐ | |||
│ │ | |||
▼ ▼ | |||
┌─────────────────┐ ┌─────────────────┐ | |||
│ PageLive │ │ HomeLive │ | |||
│ (subscribed) │ │ (not sub'd) │ | |||
│ │ │ │ | |||
│ handle_info │ │ Button click │ | |||
│ updates UI │◄──┤ broadcast() │ | |||
└─────────────────┘ └─────────────────┘ | |||
</pre> | |||
== 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: === | |||
<syntaxhighlight lang="elixir"> | |||
if connected?(socket) do | |||
Phoenix.PubSub.subscribe(App.PubSub, "events") | |||
end | |||
</syntaxhighlight> | |||
This prevents subscribing during initial static render, only subscribing once the WebSocket connects. | |||
== Running the App == | |||
# Start the Phoenix server: | |||
#: <code>mix phx.server</code> | |||
# 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. | |||
Latest revision as of 00:14, 29 January 2026
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.