How to Combine Phoenix LiveView with GraphQL for Real-Time, Reactive Interfaces
HexShift

HexShift @hexshift

About: Elixir & Phoenix enthusiast sharing advanced guides on Phoenix Framework, LiveView, WebSocket, Python and Tailwind. Helping devs build reactive, scalable apps with deep, practical insights.

Joined:
Apr 5, 2025

How to Combine Phoenix LiveView with GraphQL for Real-Time, Reactive Interfaces

Publish Date: Jun 15
0 0

GraphQL lets clients ask for exactly what they need.

Phoenix LiveView turns server state into live HTML over WebSockets.

Pair them and you get:

  • 🎯 Precise backend queries (GraphQL)
  • ⚡ Reactive, server‑rendered views (LiveView)
  • 🧩 All in Elixir — no Apollo, no Redux, no SPA bloat

1 ▸ Add Absinthe to Your Phoenix App

# mix.exs
defp deps do
  [
    {:absinthe, "~> 1.7"},
    {:absinthe_plug, "~> 1.7"},
    {:absinthe_phoenix, "~> 2.0"} # for subscriptions
  ]
end
Enter fullscreen mode Exit fullscreen mode

Create a schema:

# lib/my_app_web/schema.ex
object :user do
  field :id, non_null(:id)
  field :name, :string
  field :email, :string
  field :orders, list_of(:order)
end
Enter fullscreen mode Exit fullscreen mode

Expose it:

# router.ex
forward "/api/graphql", Absinthe.Plug, schema: MyAppWeb.Schema
Enter fullscreen mode Exit fullscreen mode

2 ▸ Query GraphQL inside LiveView

def handle_event("load_user", %{"id" => id}, socket) do
  {:ok, %{data: data}} =
    Absinthe.run(
      ~S(
        query ($id: ID!) {
          user(id: $id) {
            name
            email
            orders { total }
          }
        }
      ),
      MyAppWeb.Schema,
      variables: %{"id" => id}
    )

  {:noreply, assign(socket, user: data["user"])}
end
Enter fullscreen mode Exit fullscreen mode
  • No Apollo client
  • No JS fetch
  • Pure Elixir, pure LiveView.

3 ▸ Render the Result

<button phx-click="load_user" phx-value-id="42" class="btn">Load User</button>

<%= if @user do %>
  <div class="mt-4 p-4 border rounded">
    <h2 class="font-bold"><%= @user["name"] %></h2>
    <p class="text-sm text-gray-600"><%= @user["email"] %></p>

    <h3 class="mt-3 font-semibold">Orders</h3>
    <ul class="list-disc ml-5">
      <%= for o <- @user["orders"] do %>
        <li>$<%= o["total"] %></li>
      <% end %>
    </ul>
  </div>
<% end %>
Enter fullscreen mode Exit fullscreen mode

Tailwind keeps the UI crisp; LiveView rerenders instantly.


4 ▸ Mutations & Instant Feedback

def handle_event("update_email", %{"email" => email}, socket) do
  {:ok, %{data: %{"updateEmail" => user}}} =
    Absinthe.run("""
      mutation ($id: ID!, $email: String!) {
        updateEmail(id: $id, email: $email) { id name email }
      }
    """, MyAppWeb.Schema, variables: %{"id" => socket.assigns.user_id, "email" => email})

  {:noreply, assign(socket, user: user)}
end
Enter fullscreen mode Exit fullscreen mode

No client-side state management — LiveView assigns are the state.


5 ▸ Real‑Time Updates with GraphQL Subscriptions

# schema.ex
subscription do
  field :order_created, :order do
    config fn _, _ ->
      {:ok, topic: "*"}
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

In LiveView (thanks to absinthe_phoenix):

def mount(_, _, socket) do
  {:ok, socket} =
    Absinthe.Phoenix.Subscription.subscribe(socket, "order_created")
end

def handle_info(%{subscription_data: %{result: %{data: %{"orderCreated" => order}}}}, socket) do
  {:noreply, stream_insert(socket, :orders, order)}
end
Enter fullscreen mode Exit fullscreen mode

Every new order shows up live without polling.


6 ▸ Pagination, Filters, & Arguments

GraphQL handles vars; LiveView handles events:

<select phx-change="change_page" name="page">
  <%= for p <- 1..@total_pages do %>
    <option value={p}><%= p %></option>
  <% end %>
</select>
Enter fullscreen mode Exit fullscreen mode
def handle_event("change_page", %{"page" => p}, socket) do
  run_page_query(String.to_integer(p))
end
Enter fullscreen mode Exit fullscreen mode

7 ▸ Why This Stack Rocks

Layer Role
GraphQL Declarative data contract, fine‑grained ops
LiveView Real‑time HTML/assigns over WebSockets
Absinthe Elixir‑native execution & subscriptions
Tailwind Consistent, utility‑first styling
  • All Elixir → one codebase, one skillset
  • No SPA → faster cold start, SEO‑friendly
  • Real‑time over channels without extra plumbing

Dive Deeper

📘 Phoenix LiveView: The Pro’s Guide to Scalable Interfaces and UI Patterns

  • Server‑side GraphQL querying strategies
  • Subscriptions & Presence patterns
  • Real‑time dashboards without JS frameworks
  • Production tips for caching, auth, and error handling

Build data‑rich, real‑time interfaces without frontend bloat.

Comments 0 total

    Add comment