Claude Code for Elixir LiveView: Real-Time Server-Rendered UIs — Claude Skills 360 Blog
Blog / Backend / Claude Code for Elixir LiveView: Real-Time Server-Rendered UIs
Backend

Claude Code for Elixir LiveView: Real-Time Server-Rendered UIs

Published: January 14, 2027
Read time: 9 min read
By: Claude Skills 360

Phoenix LiveView renders HTML on the server and updates only the changed DOM over a persistent WebSocket connection — no JavaScript framework, no JSON API, no client state management. The mount/3 callback establishes state. handle_event/3 responds to user interactions. handle_info/2 processes process messages, including Phoenix PubSub broadcasts for multi-user synchronization. LiveComponents encapsulate reusable state. Streams handle large lists efficiently without holding all data in socket assigns. Claude Code generates LiveView modules, LiveComponent implementations, PubSub broadcast patterns, and the form change and submit handlers for production real-time Elixir applications.

CLAUDE.md for LiveView Projects

## LiveView Stack
- Phoenix >= 1.7.14, LiveView >= 1.0
- Real-time: Phoenix.PubSub for multi-user broadcasts
- Large lists: stream() + stream_insert/stream_delete — NOT Enum.map in assigns
- Forms: LiveView forms with phx-change + phx-submit + Ecto changesets
- Client interop: JS hooks for DOM manipulation, push_event for server→client events
- Components: function components for stateless, LiveComponent for stateful+isolated
- Auth: on_mount lifecycle hooks with Phoenix.LiveView.on_mount
- Uploads: allow_upload for server-side file handling

Basic LiveView Module

# lib/my_app_web/live/orders_live.ex
defmodule MyAppWeb.OrdersLive do
  use MyAppWeb, :live_view

  alias MyApp.Orders
  alias Phoenix.PubSub

  @impl true
  def mount(_params, _session, socket) do
    # Subscribe to order updates for real-time sync
    if connected?(socket) do
      PubSub.subscribe(MyApp.PubSub, "orders:#{socket.assigns.current_user.id}")
    end

    orders = Orders.list_orders(socket.assigns.current_user.id)

    socket =
      socket
      |> assign(:filter_status, "all")
      |> assign(:loading, false)
      |> stream(:orders, orders)    # stream for efficient large list rendering

    {:ok, socket}
  end

  @impl true
  def handle_params(%{"status" => status}, _uri, socket) do
    orders =
      socket.assigns.current_user.id
      |> Orders.list_orders(status: status)

    socket =
      socket
      |> assign(:filter_status, status)
      |> stream(:orders, orders, reset: true)

    {:noreply, socket}
  end

  def handle_params(_params, _uri, socket), do: {:noreply, socket}

  @impl true
  def handle_event("cancel_order", %{"id" => order_id}, socket) do
    case Orders.cancel_order(order_id, socket.assigns.current_user) do
      {:ok, order} ->
        # Update the specific stream entry
        {:noreply, stream_insert(socket, :orders, order)}

      {:error, reason} ->
        {:noreply, put_flash(socket, :error, "Failed: #{reason}")}
    end
  end

  def handle_event("filter", %{"status" => status}, socket) do
    {:noreply, push_patch(socket, to: ~p"/orders?status=#{status}")}
  end

  @impl true
  # Handle PubSub broadcast: new order created by another session
  def handle_info({:order_created, order}, socket) do
    {:noreply, stream_insert(socket, :orders, order, at: 0)}
  end

  # Handle PubSub broadcast: order status changed
  def handle_info({:order_updated, order}, socket) do
    {:noreply, stream_insert(socket, :orders, order)}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div class="max-w-4xl mx-auto p-6">
      <div class="flex justify-between items-center mb-6">
        <h1 class="text-2xl font-bold">Orders</h1>

        <div class="flex gap-2">
          <%= for status <- ["all", "pending", "processing", "shipped"] do %>
            <button
              phx-click="filter"
              phx-value-status={status}
              class={["px-3 py-1 rounded", @filter_status == status && "bg-blue-500 text-white"]}
            >
              <%= String.capitalize(status) %>
            </button>
          <% end %>
        </div>
      </div>

      <div id="orders" phx-update="stream" class="space-y-3">
        <%= for {dom_id, order} <- @streams.orders do %>
          <div id={dom_id} class="border rounded-lg p-4">
            <div class="flex justify-between">
              <div>
                <span class="font-mono text-sm"><%= order.id %></span>
                <p class="font-semibold">$<%= :erlang.float_to_binary(order.amount / 100, decimals: 2) %></p>
              </div>
              <div class="flex items-center gap-3">
                <span class={"badge badge-#{order.status}"}><%= order.status %></span>
                <%= if order.status == :pending do %>
                  <button
                    phx-click="cancel_order"
                    phx-value-id={order.id}
                    data-confirm="Cancel this order?"
                    class="text-red-500 hover:text-red-700 text-sm"
                  >
                    Cancel
                  </button>
                <% end %>
              </div>
            </div>
          </div>
        <% end %>
      </div>
    </div>
    """
  end
end

LiveComponent for Reusable State

# lib/my_app_web/live/components/order_form_component.ex
defmodule MyAppWeb.OrderFormComponent do
  use MyAppWeb, :live_component

  alias MyApp.Orders
  alias MyApp.Orders.Order

  @impl true
  def update(%{order: order} = assigns, socket) do
    changeset = Orders.change_order(order)

    socket =
      socket
      |> assign(assigns)
      |> assign_form(changeset)

    {:ok, socket}
  end

  @impl true
  def handle_event("validate", %{"order" => order_params}, socket) do
    changeset =
      socket.assigns.order
      |> Orders.change_order(order_params)
      |> Map.put(:action, :validate)

    {:noreply, assign_form(socket, changeset)}
  end

  def handle_event("save", %{"order" => order_params}, socket) do
    save_order(socket, socket.assigns.action, order_params)
  end

  defp save_order(socket, :new, params) do
    case Orders.create_order(socket.assigns.current_user, params) do
      {:ok, order} ->
        # Notify parent LiveView
        notify_parent({:saved, order})
        {:noreply,
          socket
          |> put_flash(:info, "Order created")
          |> push_patch(to: socket.assigns.patch)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign_form(socket, changeset)}
    end
  end

  defp save_order(socket, :edit, params) do
    case Orders.update_order(socket.assigns.order, params) do
      {:ok, order} ->
        notify_parent({:saved, order})
        {:noreply, push_patch(socket, to: socket.assigns.patch)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign_form(socket, changeset)}
    end
  end

  defp assign_form(socket, changeset) do
    assign(socket, :form, to_form(changeset))
  end

  defp notify_parent(msg), do: send(self(), {__MODULE__, msg})

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.header>
        <%= @title %>
      </.header>

      <.simple_form
        for={@form}
        id="order-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="save"
      >
        <.input field={@form[:customer_id]} type="text" label="Customer ID" />
        <.input field={@form[:amount]} type="number" label="Amount (cents)" />
        <.input
          field={@form[:status]}
          type="select"
          label="Status"
          options={[{"Pending", "pending"}, {"Processing", "processing"}]}
        />

        <:actions>
          <.button phx-disable-with="Saving...">Save Order</.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end
end

PubSub for Multi-User Updates

# lib/my_app/orders.ex — broadcast changes via PubSub
defmodule MyApp.Orders do
  alias MyApp.Repo
  alias MyApp.Orders.Order
  alias Phoenix.PubSub

  def create_order(user, attrs) do
    %Order{}
    |> Order.changeset(Map.put(attrs, "user_id", user.id))
    |> Repo.insert()
    |> tap(fn
      {:ok, order} -> broadcast_order_event(order, :order_created)
      _ -> :ok
    end)
  end

  def update_order_status(order_id, new_status) do
    order = Repo.get!(Order, order_id)

    order
    |> Order.status_changeset(%{status: new_status})
    |> Repo.update()
    |> tap(fn
      {:ok, updated} -> broadcast_order_event(updated, :order_updated)
      _ -> :ok
    end)
  end

  # Broadcast to user-specific topic
  defp broadcast_order_event(order, event) do
    PubSub.broadcast(
      MyApp.PubSub,
      "orders:#{order.user_id}",
      {event, order}
    )
  end

  # Broadcast to admin dashboard topic
  def broadcast_admin_event(order, event) do
    PubSub.broadcast(
      MyApp.PubSub,
      "admin:orders",
      {event, order}
    )
  end
end

JS Hooks for Client Interop

// assets/js/hooks.js — LiveView JS hooks
const Hooks = {}

// Hook: focus input after LiveView update
Hooks.AutoFocus = {
  updated() {
    this.el.querySelector("[data-autofocus]")?.focus()
  }
}

// Hook: infinite scroll — send load_more event when near bottom
Hooks.InfiniteScroll = {
  mounted() {
    this.observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        this.pushEvent("load_more", {})
      }
    }, { threshold: 0.1 })

    const sentinel = this.el.querySelector("[data-scroll-sentinel]")
    if (sentinel) this.observer.observe(sentinel)
  },
  destroyed() {
    this.observer?.disconnect()
  }
}

// Hook: receive server events for client-side effects
Hooks.Toast = {
  mounted() {
    this.handleEvent("show_toast", ({ message, type }) => {
      showToastNotification(message, type)
    })
  }
}

export default Hooks
# Push event from LiveView to JS hook
def handle_event("complete_checkout", _params, socket) do
  {:noreply, push_event(socket, "show_toast", %{
    message: "Order placed successfully!",
    type: "success"
  })}
end

For the Phoenix basics with channels and context modules, see the Elixir Phoenix guide for REST APIs and GenServer patterns. For the tRPC subscriptions approach that achieves similar real-time updates in TypeScript stacks using WebSocket, the tRPC subscriptions guide covers typed event streams. The Claude Skills 360 bundle includes LiveView skill sets covering real-time components, PubSub workflows, and form patterns. Start with the free tier to try LiveView module generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free