useRealtimeRoom
useRealtimeRoom
An explicit room concept that scopes useRealtimeState, useRealtimeEvents, useRealtimePresence, and useRealtimeLock together under one id, with server-side lifecycle hooks and a single auth checkpoint instead of gating every handler individually.
It's a convenience layer, not a requirement. useRealtimeLock(key, { room }) and useRealtimePresence(room) keep working standalone without ever calling useRealtimeRoom; they pass through the same server-side membership path implicitly.
Usage
<script setup lang="ts">
const room = useRealtimeRoom('document:123')
const doc = room.state('content', '')
const { members } = room.presence({ info: { name: 'Alice' } })
const { claim, release, ownedByMe } = room.lock('editing')
</script>
<template>
<div class="space-y-2">
<p>{{ Object.keys(members).length }} people here</p>
<UTextarea v-model="doc" :disabled="!ownedByMe" class="w-full" />
<UButton v-if="!ownedByMe" @click="claim()">Edit</UButton>
<UButton v-else @click="release({ changed: true })">Save</UButton>
</div>
</template>
Type Signature
function useRealtimeRoom(roomId: string): UseRealtimeRoomReturn
Parameters
roomId
- Type:
string - Required: Yes
A unique identifier for the room. Calling useRealtimeRoom joins it immediately.
Return Value
interface UseRealtimeRoomReturn {
joined: Readonly<Ref<boolean>>
join: () => Promise<boolean>
leave: () => Promise<void>
presence: <TInfo = unknown>(options?: UseRealtimePresenceOptions<TInfo>) => UseRealtimePresenceReturn<TInfo>
lock: <TOwnerInfo = string>(key: string, options?: Omit<UseRealtimeLockOptions<TOwnerInfo>, 'room'>) => UseRealtimeLockReturn<TOwnerInfo>
locks: () => UseRealtimeLockRoomReturn
state: <T>(key: string, defaultValue?: T, options?: useRealtimeStateOptions) => UseRealtimeStateReturn<T>
events: <TEventMap = Record<string, unknown>>(options?: UseRealtimeEventsOptions) => UseRealtimeEventsReturn<TEventMap>
}
joined
Whether this client is currently a member of the room, reflecting the last join/leave acknowledgment:
const room = useRealtimeRoom('document:123')
watchEffect(() => {
if (!room.joined.value) console.log('Not in the room')
})
join() / leave()
useRealtimeRoom joins automatically on setup and leaves on unmount. Call these directly for manual control:
const room = useRealtimeRoom('document:123')
async function switchDocument() {
await room.leave()
// ... navigate elsewhere ...
}
join() resolves to whether it succeeded. It's gated server-side by the nuxt-realtime:canJoinRoom hook (allowed by default).
presence()
Presence scoped to this room, equivalent to useRealtimePresence(roomId, options):
const room = useRealtimeRoom('document:123')
const { members } = room.presence({ info: { name: 'Alice' } })
lock()
A lock scoped to this room, equivalent to useRealtimeLock(key, { ...options, room: roomId }):
const room = useRealtimeRoom('document:123')
const { claim, release, ownedByMe } = room.lock('editing')
locks()
Read-only, bulk view of every lock claimed via room.lock() in this room, equivalent to useRealtimeLockRoom(roomId). Use this for an overview across many keys; use lock(key) to claim or release one specific key:
const room = useRealtimeRoom('board:main')
const { locks } = room.locks()
state()
State scoped to this room. A thin client-side convenience that namespaces key under the room id and delegates to useRealtimeState; it adds no new server protocol:
const room = useRealtimeRoom('document:123')
const title = room.state('title', 'Untitled')
events()
Event pub/sub scoped to this room. Namespaces the channel under the room id and delegates to useRealtimeEvents:
const room = useRealtimeRoom('document:123')
const { subscribe, publish } = room.events()
subscribe('comment-added', (comment) => {
console.log('New comment:', comment)
})
publish('comment-added', { text: 'Nice work!' })
Server Hooks
useRealtimeRoom fires three server-side hooks around joining and leaving. Register them in a Nitro plugin.
Gating who can join
nuxt-realtime:canJoinRoom is the single checkpoint for per-room auth, run once per connection the first time it joins a given room. Allowed by default; set ctx.allow = false to deny:
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('nuxt-realtime:canJoinRoom', async (ctx) => {
ctx.allow = await userCanAccessRoom(ctx.connectionId, ctx.roomId)
})
})
Room created / emptied
nuxt-realtime:roomCreated fires once when a room's membership goes from 0 to 1, the natural place for provisioning logic:
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('nuxt-realtime:roomCreated', async ({ roomId }) => {
await db.rooms.upsert({ id: roomId, createdAt: new Date() })
})
nitroApp.hooks.hook('nuxt-realtime:roomEmpty', async ({ roomId }) => {
await db.rooms.archive(roomId)
})
})
nuxt-realtime:roomEmpty fires once when membership goes from 1 to 0, either an explicit leave that empties the room or (after every member's disconnect grace period lapses without a reconnect) the connection registry's sweep.
Examples
Collaborative Document Page
Combine presence, a title lock, and shared state on a single document page:
<script setup lang="ts">
const props = defineProps<{ documentId: string }>()
const currentUser = useCurrentUser()
const room = useRealtimeRoom(`document:${props.documentId}`)
const { members } = room.presence({
info: { name: currentUser.name, avatarUrl: currentUser.avatarUrl },
})
const title = room.state('title', 'Untitled')
const { claim, release, ownedByMe, ownerInfo } = room.lock('title-edit')
const { subscribe, publish } = room.events()
const comments = ref([])
subscribe('comment-added', (comment) => {
comments.value.push(comment)
})
function addComment(text: string) {
publish('comment-added', { id: crypto.randomUUID(), text, author: currentUser.name })
}
</script>
<template>
<div class="space-y-4">
<UAvatarGroup>
<UAvatar
v-for="(info, connectionId) in members"
:key="connectionId"
:src="info.avatarUrl"
:alt="info.name"
:text="info.name[0]"
/>
</UAvatarGroup>
<div class="flex items-center gap-2">
<UInput v-model="title" :disabled="!ownedByMe" />
<UButton v-if="!ownedByMe" :disabled="!!ownerInfo" @click="claim()">
Edit title
</UButton>
<UButton v-else @click="release({ changed: true })">Done</UButton>
</div>
<ul class="space-y-1">
<li v-for="comment in comments" :key="comment.id">
<strong>{{ comment.author }}:</strong> {{ comment.text }}
</li>
</ul>
</div>
</template>