TanStack Query recipes
Feed shared updates into your existing query cache.
Server-sent events · Nuxt
Before you start
Section titled “Before you start”Start with an existing Nuxt app. Keep its renderer, routes and plugins. These files add one live view; Spinetab does not create your API.
Your endpoint supplies complete { "open": 12 } values: JSON in each data: field of /api/queue/events, served as text/event-stream.
The examples use same-origin URLs and declare
anonymous: true: no token is supplied, although cookies
still flow. For a separate API origin, use its URL and configure CORS or
your application's proxy. For private data, add
credentials and user scopes;
never put a secret in these files.
1. Install
Section titled “1. Install”pnpm add spinetab @tanstack/query-core@^5.104.0 @tanstack/vue-query@^5.104.0Keep your framework's existing dependencies. See compatible versions if upgrading an older app.
2. Configure your app
Section titled “2. Configure your app”Merge this addition into your configuration; preserve existing plugins and options. Restart the dev server afterwards. The plugin bundles the worker and discovers the adapters imported below.
import { defineNuxtConfig } from "nuxt/config";
export default defineNuxtConfig({ modules: ["spinetab/nuxt"] });3. Connect and render
Section titled “3. Connect and render”Save these files together in app/components/recipe/.
The provider owns a fresh QueryClient. If your app already has a provider, render Queue beneath it instead of creating another one. The query uses skipToken to read live cache updates without fetching a competing snapshot. The bridge uses that same client's ["queue"] key.
reconcile: "latest" fits this full-state contract: the next complete event restores the displayed value after a gap. Use a refresh/merge policy for deltas or partial GraphQL results. Reconnecting alone does not reconstruct missed state.
import { createSpinetab } from "spinetab";
export const spinetab = createSpinetab({ anonymous: true });export type Queue = { open: number };export type QueueView = { open?: number; problem?: string };import { sse } from "spinetab/sse";import type { Queue, QueueView } from "./queue-types";
export const queueSource = sse<Queue>("/api/queue/events");export const selectQueue = (queue: Queue): QueueView => queue;import type { QueryClient } from "@tanstack/query-core";import { bindQuery } from "spinetab/tanstack-query";import { spinetab } from "./live";import { queueSource, selectQueue } from "./source";
export function startQueue( queryClient: QueryClient, onError: (message: string) => void,) { return bindQuery(spinetab, queueSource, { queryClient, queryKey: ["queue"], map: selectQueue, reconcile: "latest", onError: (error) => onError(error.message), onEvent: () => onError(""), });}<script setup lang="ts">import { skipToken, useQuery, useQueryClient } from "@tanstack/vue-query";import { onMounted, onUnmounted, ref } from "vue";import { startQueue } from "./feed";import type { QueueView } from "./queue-types";
const client = useQueryClient();const { data } = useQuery<QueueView>({ queryKey: ["queue"], queryFn: skipToken,});const error = ref("");let binding: ReturnType<typeof startQueue> | undefined;onMounted(() => { binding = startQueue(client, (message) => { error.value = message; });});onUnmounted(() => binding?.unsubscribe());</script>
<template> <p v-if="error || data?.problem" role="alert">{{ error || data?.problem }}</p> <p v-else-if="data?.open === undefined" role="status">Loading queue…</p> <p v-else><output>{{ data.open }}</output> open</p></template><script setup lang="ts">import { QueryClient, VUE_QUERY_CLIENT } from "@tanstack/vue-query";import { onMounted, onUnmounted, provide } from "vue";import Queue from "./Queue.vue";
const client = new QueryClient();provide(VUE_QUERY_CLIENT, client);onMounted(() => client.mount());onUnmounted(() => client.unmount());</script>
<template ><Queue /></template>4. Mount the view
Section titled “4. Mount the view”<script setup lang="ts">import Recipe from "../components/recipe/Recipe.vue";</script><template ><main><Recipe /></main></template>Your existing app/app.vue must render <NuxtPage />. Work starts on browser mount; no ClientOnly wrapper is needed. KeepAlive retains subscriptions until the component is disposed.
Check the result
Section titled “Check the result”Open /queue in two tabs of the same browser profile. Both should show advancing queue values. Matching subscriptions share upstream work when SharedWorker is available; fallback runs independently in each tab. Remove one view and the other should keep updating. Removing the last view releases its subscription; connection closure can follow the adapter's idle delay.