LiveVue 1.0
Examples Phoenix Streams

Phoenix Streams

Efficiently manage large or dynamic collections with Phoenix streams. Perfect for chat messages, feeds, and real-time lists.

What this example shows

1
Phoenix Streams
stream() and stream_insert()
2
Transparent Arrays
Streams become arrays in Vue
3
Async Processing
start_async for background work
Streams.vue
<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
Next up: Connection Status
View example →