Live view and handle info: Difference between revisions

From ElixirBlocks
Jump to: navigation, search
(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:

  1. Timers: Process.send_after(self(), :tick, 1000)
  2. PubSub broadcasts: Phoenix.PubSub.broadcast(App.PubSub, "topic", :msg)
  3. Other processes: send(liveview_pid, :msg)
  4. Tasks: Background jobs completing
  5. 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/2 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 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 behaviour
  • start_link/1 - Called when the GenServer starts
  • init/1 - Initializes state and starts the timer

Timer Pattern:

  • Process.send_after(self(), :tick, @interval) - Schedules a message to ourselves
  • handle_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 children list defines all processes that should start with the app
  • The supervisor automatically starts, monitors, and restarts these processes if they crash
  • App.TimerServer uses the default child_spec (provided by use GenServer)
  • The supervisor will call App.TimerServer.start_link/1 at 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 the phx-click attribute value in the button
  • Broadcasts :my_message to 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:

  1. Application Starts:
    • Supervisor starts all children including App.TimerServer
    • TimerServer's init/1 schedules the first tick
  2. User Visits PageLive:
    • mount/3 is called
    • After WebSocket connects, subscribes to "events" topic
    • Initial state shows "Waiting for timer..."
  3. Every 3 Seconds:
    • TimerServer receives :tick via handle_info
    • Broadcasts {:timer_update, message} to "events" topic
    • PageLive receives the message in its handle_info
    • Updates @last_message assign
    • LiveView automatically re-renders with new message
  4. 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

  1. Start the Phoenix server:
    mix phx.server
  2. Visit http://localhost:4000 to see PageLive
    • Should see timer updates every 3 seconds
  3. Open HomeLive in another tab
    • Click the button to trigger a manual message to PageLive
  4. 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.