LiveVue
1.0
Phoenix Streams
Efficiently manage large or dynamic collections with Phoenix streams. Perfect for chat messages, feeds, and real-time lists.
What this example shows
<script setup lang="ts">
import { ref, nextTick, watch } from "vue"
type Message = {
id: string
role: "user" | "assistant"
content: string
}
const props = defineProps<{
messages: Message[]
isThinking: boolean
}>()
const input = ref("")
const messagesContainer = ref<HTMLElement>()
function scrollToBottom() {
nextTick(() => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
})
}
watch(() => props.messages.length, scrollToBottom)
watch(() => props.isThinking, scrollToBottom)
</script>
<template>
<div class="card bg-base-200 flex flex-col h-96">
<div ref="messagesContainer" class="flex-1 overflow-y-auto p-4 space-y-3">
<div
v-for="message in messages"
:key="message.id"
:class="[
'max-w-[80%] p-3 rounded-lg',
message.role === 'user'
? 'ml-auto bg-secondary text-white'
: 'mr-auto bg-base-300'
]"
>
{{ message.content }}
</div>
<div v-if="isThinking" class="max-w-[80%] mr-auto p-3 rounded-lg bg-base-300">
<div class="flex items-center gap-1.5">
<div class="w-2 h-2 rounded-full bg-neutral animate-bounce [animation-delay:-0.3s]"></div>
<div class="w-2 h-2 rounded-full bg-neutral animate-bounce [animation-delay:-0.15s]"></div>
<div class="w-2 h-2 rounded-full bg-neutral animate-bounce"></div>
</div>
</div>
</div>
<form phx-submit="send_message" class="p-3 border-t border-base-300 flex gap-2">
<input
v-model="input"
name="content"
type="text"
placeholder="Type a message..."
:disabled="isThinking"
class="input input-bordered flex-1 text-sm"
/>
<button
type="submit"
:disabled="isThinking || !input.trim()"
class="btn btn-secondary btn-sm"
>
Send
</button>
</form>
</div>
</template>
How it works
1 Streams are transparently converted to arrays
When you pass
@streams.messages
to a Vue component, LiveVue automatically converts it to a regular JavaScript array.
All the memory and performance benefits of streams still apply on the server side.
<.vue messages={@streams.messages} v-component="Chat" v-socket={@socket} />
2 Initialize and update streams in LiveView
Use stream/3
in mount to initialize,
and
stream_insert/3
to add items.
Only the diff is sent over the wire, making updates very efficient.
# Initialize
|> stream(:messages, [])
# Insert
|> stream_insert(:messages, new_message)
3 Use start_async for background work
When processing takes time (like AI responses), use
start_async/3
to run work in the background. This keeps the UI responsive and allows
props to update during processing.
socket
|> assign(is_thinking: true)
|> start_async(:ai_response, fn ->
# Background work here
generate_response()
end)
4 Handle async results
The handle_async/3
callback receives the result when the background task completes.
Update your assigns here to reflect the new state.
def handle_async(:ai_response, {:ok, response}, socket) do
{:noreply,
socket
|> stream_insert(:messages, response)
|> assign(is_thinking: false)}
end