Vue
Use Lunora with Vue 3 — a client plugin, live useQuery/useMutation composables, and the SSR-seed → live reactive-loader handoff.
Last updated:
@lunora/vue is the Vue 3 adapter, an idiomatic layer over
@lunora/client that re-expresses the live contract as
composables. A live query is a ref the socket writes to. An optimistic mutation
is a bundle of refs plus an awaitable mutate.
This page is the task-oriented guide; for the full reference see
@lunora/vue.
Preview. The Vue adapter exposes the same hydrate-then-subscribe handoff as React, but it is preview maturity: not yet proven end-to-end against a running app. See Bring your framework.
Install
pnpm add @lunora/vuenpm install @lunora/vueyarn add @lunora/vuebun add @lunora/vueProvide the client
Mount one app-wide LunoraClient with the createLunora plugin (or
provideLunora(client) inside a parent setup() to scope a subtree). Read it
anywhere with useLunora().
import { LunoraClient } from "lunorash/client";
import { createLunora } from "@lunora/vue";
import { createApp } from "vue";
import App from "./App.vue";
const client = new LunoraClient({ url: import.meta.env.VITE_LUNORA_URL });
createApp(App).use(createLunora(client)).mount("#app");Live queries
useQuery(fn, args) subscribes and exposes the latest value as a ref
(undefined until the first response). Pass args as a plain value, a ref, or
a getter; a reactive source re-subscribes when it changes. Pass "skip" to
short-circuit.
<script setup lang="ts">
import { useQuery } from "@lunora/vue";
import { ref } from "vue";
import { api } from "./lunora/_generated/api";
const channelId = ref("general");
// Reactive getter args: changing `channelId` re-subscribes.
const messages = useQuery(api.messages.list, () => ({ channelId: channelId.value }));
</script>
<template>
<ul>
<li v-for="m in messages" :key="m._id">{{ m.text }}</li>
</ul>
</template>Pass { shardKey } in options to target a specific shard when the function is
.shardBy(...)-partitioned. Call useQuery inside setup() so the subscription
tears down on unmount.
Mutations
useMutation(fn) returns a MutationHandle of refs:
{ data, error, pending, mutate, reset }. pending is ref-counted across
overlapping calls. You pass optimistic updates per call (the Vue adapter
has no bound withOptimisticUpdate).
<script setup lang="ts">
import { useMutation } from "@lunora/vue";
import { api } from "./lunora/_generated/api";
const { mutate, pending, error } = useMutation(api.messages.send);
async function send(channelId: string, text: string) {
await mutate(
{ channelId, text },
{
optimisticUpdate: (store) => {
const current = store.getQuery(api.messages.list, { channelId }) ?? [];
store.setQuery(api.messages.list, { channelId }, [...current, { _id: "tmp", text }]);
},
},
);
}
</script>
<template>
<button :disabled="pending" @click="send('general', 'hi')">Send</button>
<p v-if="error">{{ error.message }}</p>
</template>Reactive loaders
Run the query on the server with the socket-free @lunora/vue/server entry, hand
the serializable Preloaded token to the client, and hydratePreloaded seeds a
ref synchronously. The first read shows the server value with no loading
flash, then the live subscription attaches.
import { createServerClient, preloadQuery } from "@lunora/vue/server";
import { api } from "./lunora/_generated/api";
// Per request — never reuse a client across requests (token leakage).
const client = createServerClient({ url: process.env.LUNORA_URL!, token });
const preloaded = await preloadQuery(client, api.posts.list, {});<script setup lang="ts">
import { hydratePreloaded } from "@lunora/vue";
import type { Preloaded } from "@lunora/vue";
const props = defineProps<{ preloaded: Preloaded<Array<{ _id: string; title: string }>> }>();
// Seeded from SSR on first read, then live.
const posts = hydratePreloaded(props.preloaded);
</script>
<template>
<ul>
<li v-for="p in posts" :key="p._id">{{ p.title }}</li>
</ul>
</template>See Reactive loaders for the full handoff.
One worker, one deploy
@lunora/vue/worker re-exports withLunora for frameworks that expose their
emitted fetch handler as an importable module.
Nuxt caveat. Nitro does not expose its fetch handler as an importable module, so single-worker composition is not achievable for Nuxt. The supported
Nuxt integration is a two-worker split: the Nuxt/Nitro SSR worker plus a standalone Lunora worker owning /_lunora/* + ShardDO, wired by
NUXT_PUBLIC_LUNORA_URL.
See also
- @lunora/vue — the full composable + server reference
- Bring your framework — composition + adapter maturity
- Reactive loaders — the SSR-seed → live handoff
- Real-time — how subscriptions and deltas work