useRealtimeEvents
useRealtimeEvents
Publish and subscribe to events across clients in real-time. Perfect for notifications, chat messages, live updates, or any ephemeral communication that doesn't need to be persisted.
Usage
<script setup>
const { subscribe, publish } = useRealtimeEvents()
// Subscribe to events
subscribe('notifications', (data) => {
console.log('Received:', data)
})
// Publish an event
function notify() {
publish('notifications', { message: 'Hello!' })
}
</script>
Type Signature
function useRealtimeEvents<TEventMap = Record<string, unknown>>(
options?: UseRealtimeEventsOptions
): UseRealtimeEventsReturn<TEventMap>
Options
interface UseRealtimeEventsOptions {
/**
* Timeout for publish acknowledgments in milliseconds
* @default 5000
*/
publishTimeout?: number
/**
* Middleware functions applied to every incoming event before subscribers are called.
* Each middleware receives the mutable event object and a `next` function.
* Mutate `event.data` to transform the payload; omit calling `next()` to block the event.
*/
middleware?: EventMiddleware[]
}
type EventMiddleware = (
event: { channel: string; data: unknown },
next: () => void
) => void
Return Value
interface UseRealtimeEventsReturn<TEventMap = Record<string, unknown>> {
subscribe: <K extends string>(
channel: K,
callback: (data: K extends keyof TEventMap ? TEventMap[K] : unknown, actualChannel?: string) => void,
options?: UseRealtimeEventsSubscribeOptions
) => () => void
publish: <K extends string & keyof TEventMap>(
channel: K,
data: TEventMap[K],
options?: UseRealtimeEventsSubscribeOptions
) => Promise<void>
unsubscribe: (channel: string) => void
}
Methods
subscribe()
Subscribe to events on a channel. Supports exact channel names, namespace wildcards (chat:*), and the global wildcard (*).
When using a wildcard pattern, the callback receives the actual matched channel name as a second argument.
subscribe<K>(
channel: K,
callback: (data: unknown, actualChannel?: string) => void,
options?: UseRealtimeEventsSubscribeOptions
): () => void
Parameters:
| Parameter | Type | Description |
|---|---|---|
channel | string | Channel name, namespace:*, or * |
callback | (data, actualChannel?) => void | Function called when an event is received |
options | UseRealtimeEventsSubscribeOptions | Optional settings |
Returns: An unsubscribe function
const { subscribe } = useRealtimeEvents()
// Basic subscription
subscribe('chat', (message) => {
console.log('New message:', message)
})
// With TypeScript
interface ChatMessage {
user: string
text: string
timestamp: number
}
subscribe<ChatMessage>('chat', (message) => {
console.log(`${message.user}: ${message.text}`)
})
// Store unsubscribe function
const unsubscribe = subscribe('channel', callback)
// Later, unsubscribe
unsubscribe()
publish()
Publish an event to a channel.
publish<T>(
channel: string,
data: T,
options?: UseRealtimeEventsSubscribeOptions
): Promise<void>
Parameters:
| Parameter | Type | Description |
|---|---|---|
channel | string | The channel name to publish to |
data | T | The event data to send |
options | UseRealtimeEventsSubscribeOptions | Optional settings |
Returns: A Promise that resolves when the server acknowledges the event
const { publish } = useRealtimeEvents()
// Basic publish
await publish('notifications', { message: 'Hello!' })
// With error handling
try {
await publish('chat', {
user: 'Alice',
text: 'Hello everyone!',
timestamp: Date.now(),
})
} catch (error) {
console.error('Failed to send message:', error)
}
unsubscribe()
Unsubscribe from all callbacks on a channel.
unsubscribe(channel: string): void
const { subscribe, unsubscribe } = useRealtimeEvents()
subscribe('channel', callback1)
subscribe('channel', callback2)
// Remove all subscriptions for this channel
unsubscribe('channel')
Wildcard Subscriptions
Subscribe to multiple channels at once using wildcard patterns.
Namespace wildcard (namespace:*)
Matches any channel that starts with the given prefix:
const { subscribe, publish } = useRealtimeEvents()
// Receives chat:message, chat:typing, chat:leave — any channel starting with 'chat:'
subscribe('chat:*', (data, channel) => {
console.log(`Event on ${channel}:`, data)
})
await publish('chat:message', { text: 'Hello' }) // triggers the wildcard subscriber
await publish('chat:typing', { userId: '1' }) // triggers the wildcard subscriber
await publish('notifications', { msg: 'Alert' }) // does NOT trigger
Global wildcard (*)
Receives every event published on any channel:
subscribe('*', (data, channel) => {
console.log(`[${channel}]`, data)
})
Exact and wildcard together
Exact and wildcard subscribers for the same channel each receive the event once:
subscribe('chat:message', (data) => { /* exact */ })
subscribe('chat:*', (data, channel) => { /* wildcard */ })
await publish('chat:message', { text: 'Hi' }, { includeSelf: true })
// Both callbacks fire exactly once
chat:* matches chat:message, chat:typing, and chat:room:message — any channel whose name starts with chat:.Event Middleware
Middleware runs before any subscriber callbacks and receives a mutable event object. Call next() to continue the pipeline; omit it to block the event entirely.
const { subscribe, publish } = useRealtimeEvents({
middleware: [
// Validation — block events that fail a check
(event, next) => {
if (isValid(event.data)) next()
},
// Transformation — mutate event.data before subscribers see it
(event, next) => {
event.data = { ...event.data, receivedAt: Date.now() }
next()
},
],
})
Middleware is applied to every incoming event for the composable instance that declares it.
Type-Safe Events
Pass an event map as a generic type parameter to get compile-time type checking on channel names and their payloads:
interface AppEvents {
'chat:message': { text: string; userId: string }
'chat:typing': { userId: string }
'user:join': { userId: string; name: string }
}
const { publish, subscribe } = useRealtimeEvents<AppEvents>()
// publish — channel and data are both fully typed
publish('chat:message', { text: 'Hello', userId: '1' }) // ✓
publish('chat:message', { text: 'Hello' }) // ✗ missing userId
publish('unknown:channel', { text: 'Hello' }) // ✗ not in AppEvents
// subscribe — data parameter is automatically typed
subscribe('chat:message', (data) => {
console.log(data.text, data.userId) // typed as { text: string; userId: string }
})
Subscribe Options
interface UseRealtimeEventsSubscribeOptions {
/**
* Whether to receive events that this client published
* @default false
*/
includeSelf?: boolean
}
includeSelf
By default, when you publish an event, you don't receive it back. Set includeSelf: true to receive your own events:
const { subscribe, publish } = useRealtimeEvents()
// Won't receive own events (default)
subscribe('chat', (msg) => console.log(msg))
publish('chat', { text: 'Hello' }) // callback NOT triggered
// Will receive own events
subscribe('chat', (msg) => console.log(msg), { includeSelf: true })
publish('chat', { text: 'Hello' }) // callback IS triggered
// Can also set on publish
publish('chat', { text: 'Hello' }, { includeSelf: true })
Examples
Chat Messages
Real-time chat with message history:
<script setup>
const { subscribe, publish } = useRealtimeEvents()
const messages = ref([])
const input = ref('')
const userId = crypto.randomUUID()
interface Message {
id: string
userId: string
text: string
timestamp: number
}
subscribe<Message>('chat-messages', (message) => {
messages.value.push(message)
})
async function sendMessage() {
if (!input.value.trim()) return
await publish('chat-messages', {
id: crypto.randomUUID(),
userId,
text: input.value,
timestamp: Date.now(),
})
input.value = ''
}
</script>
<template>
<div>
<div class="messages">
<div
v-for="msg in messages"
:key="msg.id"
:class="msg.userId === userId ? 'own' : 'other'"
>
{{ msg.text }}
</div>
</div>
<form @submit.prevent="sendMessage">
<input v-model="input" placeholder="Type a message..." />
<button type="submit">Send</button>
</form>
</div>
</template>
Toast Notifications
Broadcast notifications to all clients:
<script setup>
const toast = useToast()
const { subscribe, publish } = useRealtimeEvents()
interface Notification {
title: string
description: string
color: 'success' | 'info' | 'warning' | 'error'
}
subscribe<Notification>('notifications', (notification) => {
toast.add({
title: notification.title,
description: notification.description,
color: notification.color,
})
})
function notifyAll(type: Notification['color']) {
publish('notifications', {
title: `${type.charAt(0).toUpperCase() + type.slice(1)} Alert`,
description: 'This notification was broadcast to all clients.',
color: type,
})
}
</script>
<template>
<div class="flex gap-2">
<button @click="notifyAll('success')">Success</button>
<button @click="notifyAll('info')">Info</button>
<button @click="notifyAll('warning')">Warning</button>
<button @click="notifyAll('error')">Error</button>
</div>
</template>
Live Cursors
Show other users' cursor positions:
<script setup>
const { subscribe, publish } = useRealtimeEvents()
const cursors = ref(new Map())
const myId = crypto.randomUUID()
interface CursorPosition {
userId: string
x: number
y: number
color: string
}
subscribe<CursorPosition>('cursors', (cursor) => {
if (cursor.userId !== myId) {
cursors.value.set(cursor.userId, cursor)
}
})
function handleMouseMove(event: MouseEvent) {
publish('cursors', {
userId: myId,
x: event.clientX,
y: event.clientY,
color: '#' + myId.slice(0, 6),
})
}
onMounted(() => {
window.addEventListener('mousemove', handleMouseMove)
})
onUnmounted(() => {
window.removeEventListener('mousemove', handleMouseMove)
})
</script>
<template>
<div>
<div
v-for="[id, cursor] in cursors"
:key="id"
class="cursor"
:style="{
left: cursor.x + 'px',
top: cursor.y + 'px',
backgroundColor: cursor.color,
}"
/>
</div>
</template>
Typing Indicators
Show when other users are typing:
<script setup>
const { subscribe, publish } = useRealtimeEvents()
const typingUsers = ref(new Set())
const myId = crypto.randomUUID()
let typingTimeout: ReturnType<typeof setTimeout>
subscribe<{ userId: string; isTyping: boolean }>('typing', ({ userId, isTyping }) => {
if (userId === myId) return
if (isTyping) {
typingUsers.value.add(userId)
} else {
typingUsers.value.delete(userId)
}
})
function onInput() {
publish('typing', { userId: myId, isTyping: true })
clearTimeout(typingTimeout)
typingTimeout = setTimeout(() => {
publish('typing', { userId: myId, isTyping: false })
}, 1000)
}
</script>
<template>
<div>
<input @input="onInput" placeholder="Type something..." />
<p v-if="typingUsers.size > 0">
{{ typingUsers.size }} user(s) typing...
</p>
</div>
</template>
Differences from useRealtimeState
| Feature | useRealtimeState | useRealtimeEvents |
|---|---|---|
| Persistence | State persisted on server | Events are ephemeral |
| Initial Value | Loaded from server on mount | No initial value |
| Use Case | Shared data (counters, lists) | Notifications, messages |
| API Style | Reactive ref | Pub/sub callbacks |
Use useRealtimeState when you need persistent, shared state. Use useRealtimeEvents when you want to broadcast ephemeral messages or notifications.