PhoenixVapor compiles Vue template syntax into native LiveView rendered trees. Four progressive modes share a common foundation: Vue/Vapor templates and LiveView rendered diffs have the same statics/dynamics shape.
Core: Statics/Dynamics Split
A Vue template compiles into a split — static HTML fragments and dynamic insertion points:
Template: <div class="card"><h1>{{ title }}</h1><p>{{ body }}</p></div>
Statics: ["<div class=\"card\"><h1>", "</h1><p>", "</p></div>"]
Dynamics: [title, body]This maps directly to %Phoenix.LiveView.Rendered{}:
- Statics sent once per fingerprint, cached by the client
- Only changed dynamics travel the wire on updates
v-if→ nested%Rendered{}v-for→%Comprehension{}
Expression Evaluation
Template expressions are evaluated against LiveView assigns:
- Simple (
{{ count }},item.name) → Elixir map access via OXC AST - Complex (
.filter(),.map(), arrow functions) → QuickBEAM JS eval - Change tracking: each slot knows which assigns it depends on (compile-time AST analysis)
Mode 1: ~VUE Sigil
Vue syntax as a template DSL. Zero client JS.
Template → Vize.vapor_split! → %Rendered{} → LiveView diff → morphdomExpressions evaluate in Elixir. Events map to phx-click, phx-submit, etc. The browser runs the standard LiveView client — it doesn't know Vue exists.
Mode 2: Reactive (.vue SFC)
Server-side Vue reactivity via QuickBEAM. Zero client JS.
SFC → ScriptSetup.parse → Runtime (GenServer + QuickBEAM)
→ Template → %Rendered{} with reactive state from JS runtimeA persistent QuickBEAM context per LiveView holds ref() values, computed() definitions, and function handlers. Vue's @vue/reactivity runs server-side on the BEAM. State survives across events but resets on process restart.
Mode 3: Hybrid (.vue SFC)
Split reactivity — server owns data, client owns UI state.
SFC → Classifier (AST analysis)
→ Server: mount/render/handle_event (Elixir)
→ Client: Vue 3 component (JS, ~50KB)
→ Bridge: LiveView hook syncs props via data attributesThe compiler analyzes <script setup> and classifies each binding:
| Pattern | Classification |
|---|---|
defineProps(["x"]) | Server prop |
ref(value) | Client ref |
computed using any prop | Mixed computed |
Function with "use server" | Server action |
| Function writing to a prop | Server action (auto-detected) |
| Function writing only to refs | Client handler |
Server renders full HTML for first paint (SEO). Client hydrates with Vue 3 createApp, taking over reactive slots. Client interactions (search, sort, select) are instant — zero network. Server actions send events over the existing LiveView WebSocket.
Wire Protocol
Initial render: server sends statics + dynamics + props JSON in data-pv-props attribute. Updates: only changed server slots + updated props JSON travel the wire. Client-only changes (typing in search) produce zero wire traffic.
State Sync
- Server → Client: LiveView assign changes → re-render → diff with props JSON → hook's
updated()→__applyProps()→ Vue reactivity propagates - Client → Server:
"use server"function →pushEvent→handle_event→ assign change → back to step 1 - Client → Client:
refmutation → computed recomputation → Vue re-render. No wire.
Custom Elixir Code
A hybrid module is a standard LiveView. The use PhoenixVapor macro generates render/1 and fallback handle_event/3 stubs (via @before_compile) — everything else is yours to define. User-defined handle_event clauses take precedence over generated fallbacks.
The "use server" directive in the .vue file serves two purposes:
- Tells the client codegen to generate a
pushEventcall for that function name - Registers the event name so a fallback
handle_eventis generated if the developer doesn't write one
The developer writes the actual server logic in Elixir:
def handle_event("deleteContact", %{"id" => id}, socket) do
Repo.delete!(Contact, id)
{:noreply, assign(socket, contacts: Repo.all(Contact))}
endAll standard LiveView callbacks work: mount/3, handle_info/2, handle_params/3, terminate/2. PubSub subscriptions, presence, streams — the full LiveView toolkit is available.
Mode 4: Full Vue Runtime
Third-party Vue component libraries rendered server-side in QuickBEAM.
SFC + bundle → VueRuntime (GenServer + QuickBEAM + lexbor DOM)
→ HTML string → %Rendered{} → LiveView diffFull Vue semantics: provide/inject, component composition, ARIA attributes. Used for libraries like Reka UI.
Module Map
Template / Render
PhoenixVapor— main entry,__using__macroPhoenixVapor.Sigil—~VUEsigilPhoenixVapor.Renderer— Vapor IR →%Rendered{}PhoenixVapor.Expr— JS expression evaluation in ElixirPhoenixVapor.Component— component helpersPhoenixVapor.Vue—.vuefile loading
Reactive
PhoenixVapor.Reactive— server-side reactivity implementationPhoenixVapor.Runtime— QuickBEAM GenServer for reactive statePhoenixVapor.ScriptSetup—<script setup>parsing
Hybrid
PhoenixVapor.Hybrid— split reactivity implementationPhoenixVapor.Hybrid.Classifier— binding classification via ASTPhoenixVapor.Hybrid.ServerCodegen— Elixir code generationPhoenixVapor.Hybrid.ClientCodegen— Vue 3 JS generation
Full Runtime
PhoenixVapor.LiveVue— full Vue runtime (runtime: :full)PhoenixVapor.VueRuntime— QuickBEAM GenServer for full Vue
Client JS
priv/js/hybrid-bridge.js— LiveView hook for hybrid modepriv/js/vue-reactivity.js—@vue/reactivityfor server-side reactive modepriv/js/runtime-setup.js— QuickBEAM reactive runtime bootstrap