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/vue
npm install @lunora/vue
yarn add @lunora/vue
bun add @lunora/vue

Provide 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().

src/main.ts
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.

server (Nuxt/Nitro route)
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 Deploy your framework.

See also