We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Use `send_update_after/4` for delayed LiveComponent updates
almirsarajcic
        
Manual timer management with Process.send_after/3 in LiveComponents creates cleanup complexity. LiveView’s send_update_after/4 handles delayed updates automatically without timer tracking.
# ✅ BUILT-IN DELAY - Automatic timer management
defmodule NotificationComponent do
  use Phoenix.LiveComponent
  def render(assigns) do
    ~H"""
    <div :if={@show} class="notification">
      <%= @message %>
    </div>
    """
  end
  def update(%{message: message}, socket) do
    # Show notification, auto-hide after 5 seconds
    send_update_after(__MODULE__, [id: socket.assigns.id, show: false], 5_000)
    {:ok, assign(socket, message: message, show: true)}
  end
  def update(%{show: false}, socket) do
    {:ok, assign(socket, show: false)}
  end
end
# Usage in parent LiveView
def handle_event("save", params, socket) do
  send_update(NotificationComponent, id: "notif", message: "Saved!")
  {:noreply, socket}
end
Without send_update_after/4, you need manual timer references and cleanup:
# ❌ MANUAL TIMERS - Complex cleanup required
defmodule NotificationComponent do
  use Phoenix.LiveComponent
  def render(assigns) do
    ~H"""
    <div :if={@show} class="notification">
      <%= @message %>
    </div>
    """
  end
  def update(%{message: message}, socket) do
    # Cancel previous timer if exists
    if socket.assigns[:timer_ref] do
      Process.cancel_timer(socket.assigns.timer_ref)
    end
    # Set new timer and track reference
    timer_ref = Process.send_after(self(), {:hide_notification, socket.assigns.id}, 5_000)
    {:ok, assign(socket, message: message, show: true, timer_ref: timer_ref)}
  end
  # Parent LiveView needs to handle the message
  def handle_info({:hide_notification, component_id}, socket) do
    send_update(NotificationComponent, id: component_id, show: false)
    {:noreply, socket}
  end
end
How send_update_after works
send_update_after/4 schedules a component update and returns a cancellable reference:
# Schedule update and get reference
ref = send_update_after(StatusComponent, [id: "status", status: :idle], 10_000)
# Cancel if needed
Process.cancel_timer(ref)
The timer is automatically cleaned up when:
- The LiveView process terminates
- The component is removed from the DOM
- A new update overrides the pending state
Common patterns
Auto-refresh components:
defmodule LiveStatsComponent do
  use Phoenix.LiveComponent
  def mount(socket) do
    # Refresh every 30 seconds
    schedule_refresh()
    {:ok, assign(socket, stats: load_stats())}
  end
  def update(assigns, socket) do
    if assigns[:refresh] do
      schedule_refresh()
      {:ok, assign(socket, stats: load_stats())}
    else
      {:ok, assign(socket, assigns)}
    end
  end
  defp schedule_refresh do
    send_update_after(__MODULE__, [id: "stats", refresh: true], 30_000)
  end
end
Debounced state changes:
defmodule SearchComponent do
  use Phoenix.LiveComponent
  def handle_event("search", %{"query" => query}, socket) do
    # Update immediately for UI feedback
    socket = assign(socket, query: query, searching: true)
    # Debounce actual search by 500ms
    send_update_after(__MODULE__, [id: socket.assigns.id, execute_search: query], 500)
    {:noreply, socket}
  end
  def update(%{execute_search: query}, socket) do
    results = perform_search(query)
    {:ok, assign(socket, results: results, searching: false)}
  end
end
Parent LiveView triggering delayed updates:
defmodule DashboardLive do
  use Phoenix.LiveView
  def handle_event("start-background-job", params, socket) do
    # Start job immediately
    {:ok, job} = start_job(params)
    send_update(StatusComponent, id: "status", status: :running)
    # Auto-reset status after job completes (estimated 10 seconds)
    send_update_after(StatusComponent, [id: "status", status: :idle], 10_000)
    {:noreply, assign(socket, current_job: job)}
  end
  def handle_event("show-temp-message", %{"text" => text}, socket) do
    # Show message in component, auto-hide after 3 seconds
    send_update(MessageComponent, id: "msg", text: text, visible: true)
    send_update_after(MessageComponent, [id: "msg", visible: false], 3_000)
    {:noreply, socket}
  end
end
Pro tip: Use send_update_after/4 instead of combining Process.send_after/3 with handle_info/2 for cleaner component-scoped delays.
copied to clipboard
