Learn fundamental patterns for using Vue components in Phoenix LiveView
This guide covers the essential patterns for integrating Vue components within Phoenix LiveView, including component rendering, props handling, event management, and more.
When passing custom structs as props, you must implement the LiveVue.Encoder protocol:
defmodule User do @derive LiveVue.Encoder defstruct [:name, :email, :age]end# Use in your LiveViewdef render(assigns) do ~H""" <.vue user={@current_user} v-component="UserProfile" v-socket={@socket} /> """end
The encoder protocol ensures that only specified fields are sent to the client, sensitive data is protected, and props can be efficiently diffed for updates.
If you get a Protocol.UndefinedError when passing structs as props, it means you need to implement the LiveVue.Encoder protocol for that struct. This is a safety feature to prevent accidental exposure of sensitive data.
There are two ways to access the Phoenix LiveView hook instance from your Vue components:
useLiveVue() composable
$live property
Use the useLiveVue() composable when you need to access the hook instance for logic within your <script setup> block. It’s ideal for pushing events programmatically.
<script setup>import { useLiveVue } from "live_vue"import { ref } from "vue"const live = useLiveVue()const name = ref("")function save() { live.pushEvent("save", { name: name.value })}</script>
To listen for events from the server, the easiest way is to use the useLiveEvent composable, which will automatically handle cleanup for you.
<script setup>import { useLiveEvent } from "live_vue"// Example: listening for a server eventuseLiveEvent("response", (payload) => { console.log(payload) })</script>
For convenience, the hook instance is also available directly in your template as the $live property. This is the preferred method for simple, one-off event pushes directly from the template.
<template> <button @click="$live.pushEvent('hello', { value: 'world' })"> Click me </button></template>
For navigation, LiveVue provides a built-in Link component that makes using live_patch and live_redirect as easy as using a standard <a> tag.
<script setup>import { Link } from "live_vue"</script><template> <nav> <!-- Behaves like a normal link --> <Link href="/about">About</Link> <!-- Performs a `live_patch` --> <Link patch="/users?sort=name">Sort by Name</Link> <!-- Performs a `live_redirect` --> <Link navigate="/dashboard">Dashboard</Link> </nav></template>
If you want to create reusable Vue components where you’d like to define what happens when Vue emits an event, you can use the v-on: syntax with JSmodule helpers.
Vue components can receive slots from LiveView templates:
<.Card title="Example Card" v-socket={@socket}> <p>This is the default slot content!</p> <p>Phoenix components work too: <.icon name="hero-info" /></p> <:footer> This is a named slot </:footer></.Card>
Each slot is wrapped in a div (technical limitation)
You can use HEEX components inside slots
Slots stay reactive and update when their content changes
Hooks inside slots are not supportedSlots are rendered server-side and then sent to the client as a raw HTML. It happens outside of the LiveView lifecycle, so hooks inside slots are not supported.As a consequence, since .vue components rely on hooks, it’s not possible to nest .vue components inside other .vue components.
LiveVue provides seamless integration with Phoenix LiveView’s file upload system through the useLiveUpload() composable. This handles all the complexity of managing upload state, progress tracking, and DOM elements automatically.
LiveVue provides full support for Phoenix LiveView’s stream() operations. Since LiveVue already sends minimal JSON patches for all props updates, streams primarily help reduce server memory consumption by not keeping large collections in the socket assigns.
Lists with many items where you want to avoid keeping all data in memory
Real-time data like chat messages, notifications, or live feeds
LiveVue’s automatic JSON patch diffing already ensures efficient client updates for regular props, so the main benefit of streams is server-side memory efficiency.
The ~VUE sigil provides an alternative to the standard LiveView DSL, allowing you to write Vue components directly in your LiveView:
The ~V sigil is deprecated in favor of ~VUE. It will be removed in future versions.
defmodule MyAppWeb.CounterLive do use MyAppWeb, :live_view def render(assigns) do ~VUE""" <script setup lang="ts"> import {ref} from "vue" const props = defineProps<{count: number}>() const diff = ref(1) </script> <template> Current count: {{ props.count }} <label>Diff: </label> <input v-model.number="diff" type="range" min="1" max="10" /> <button phx-click="inc" :phx-value-diff="diff"> Increase counter by {{ diff }} </button> </template> """ end def mount(_params, _session, socket) do {:ok, assign(socket, count: 0)} end def handle_event("inc", %{"diff" => diff}, socket) do {:noreply, update(socket, :count, &(&1 + String.to_integer(diff)))} endend
The ~VUE sigil is a powerful macro that compiles the string content into a full-fledged Vue component at compile time. It automatically passes all of the LiveView’s assigns as props to the component.