Skip to content

Nuxt

This guide uses Nuxt 4 with Vite and its default app/ directory. It adds a shared live feed to an existing app without disabling server rendering.

The examples display a queue value such as { "open": 12 }. Choose the source that matches your backend in step 3. Use your existing server route or replace the URL and type with your API. Spinetab does not create the endpoint.

Use a complete recipe for your renderer and data library, or continue below for the basic direct subscription.

Open recipe

Terminal window
pnpm add spinetab

Add the module alongside your existing modules:

nuxt.config.ts
import { defineNuxtConfig } from "nuxt/config";
export default defineNuxtConfig({
modules: ["spinetab/nuxt"],
});

The module configures the Vite plugin for the client build and generates the worker. You do not need to add spinetab/vite separately. Restart the dev server after changing the config.

app/live.ts
import { createSpinetab } from "spinetab";
import { bindClient } from "spinetab/vue";
export const spinetab = createSpinetab();
export const { useLive } = bindClient(spinetab);

Import the bound composable explicitly from components. No Nuxt runtime plugin or provider is required for this setup.

Choose the format your server already serves. Save one of these as queue-source.ts alongside live.ts. Each example receives the complete queue value, { "open": 12 }, so the component below stays the same.

Read a JSON response from GET /api/queue every five seconds by default.

queue-source.ts
import { polling } from "spinetab/polling";
export const queueSource = polling<{ open: number }>("/api/queue");

Polling options

These URLs are examples, not routes created by the plugin. Replace them with your API. For GraphQL, Socket.IO or a client library, see how sources and integrations fit together.

app/components/Queue.vue
<script setup lang="ts">
import { queueSource } from "../queue-source";
import { useLive } from "../live";
const { data, error } = useLive(queueSource, {
reconcile: "latest",
});
</script>
<template>
<p v-if="error" role="alert">Could not load the queue: {{ error.code }}</p>
<p v-else-if="data === undefined" role="status">Loading queue…</p>
<p v-else>{{ data.open }} open</p>
</template>

Render it from a page:

app/pages/queue.vue
<script setup lang="ts">
import Queue from "../components/Queue.vue";
</script>
<template>
<main>
<h1>Queue</h1>
<Queue />
</main>
</template>

If you have an app/app.vue, keep its layout and ensure it renders <NuxtPage /> for page routing.

The composable renders the loading state on the server, starts its subscription on mount in the browser and releases it when the component’s scope is disposed. A <ClientOnly> wrapper is not required. A page cached by <KeepAlive> retains its subscription until it is disposed; deactivation is not unmounting.

Visit /queue in your running app. Navigate away in one tab while keeping the other on the queue page; that remaining subscriber continues receiving updates.

Open the page in two tabs of the same browser profile. With SharedWorker available, matching subscriptions share the upstream feed or polling schedule. Each component keeps its own value. For polling, a new subscriber can trigger a fresh read; check ongoing requests rather than expecting exactly one initial request.

Polling reads every five seconds by default and pauses when no consumer is eligible, such as when all subscribing tabs are hidden. It reads again when a tab returns. If worker sharing is unavailable, each tab runs its own subscriptions; see execution modes.

If you introduce a new adapter during development, restart the dev server if Nuxt needs to prepare its dependencies. See the Nuxt development restriction for the WebKit cache caveat.

reconcile: "latest" fits these full-state feeds. After a delivery gap, the next value restores the displayed state. Use a refresh policy for feeds of incremental changes.

Pass plugin options through the module tuple; see Nuxt configuration.