Next.js
This guide adds a shared live feed to an existing Next.js App Router app.
It works with Turbopack and webpack. Your page can remain a Server Component;
the component that subscribes uses "use client".
The examples display a queue value such as { "open": 12 }. Choose the source
that matches your backend in step 3.
Use an API endpoint that supports your chosen transport. Next.js route handlers
can serve HTTP responses; a WebSocket feed needs a WebSocket-capable backend.
The plugin does not create these endpoints.
Choose your stack
Section titled “Choose your stack”Use a complete recipe for your renderer and data library, or continue below for the basic direct subscription.
1. Wrap your Next.js config
Section titled “1. Wrap your Next.js config”pnpm add spinetabPass your existing config to withSpinetab:
import type { NextConfig } from "next";import { withSpinetab } from "spinetab/next";
const nextConfig: NextConfig = { // Keep your existing options here.};
export default withSpinetab(nextConfig);The plugin handles the worker build. Restart next dev after changing the config.
You do not need transpilePackages or dynamic(..., { ssr: false }) for Spinetab.
2. Create the client
Section titled “2. Create the client”import { createSpinetab } from "spinetab";import { bindClient } from "spinetab/react";
export const spinetab = createSpinetab();export const { useLive, useSpinetabStatus } = bindClient(spinetab);Import this module only from Client Components or other client modules. If your
project uses src/app/, put these files there instead.
3. Choose your source
Section titled “3. Choose your source”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.
import { polling } from "spinetab/polling";
export const queueSource = polling<{ open: number }>("/api/queue");Your endpoint serves text/event-stream with JSON in each data: field.
import { sse } from "spinetab/sse";
export const queueSource = sse<{ open: number }>("/api/queue/events");Your WebSocket endpoint sends a JSON queue value in each text frame. This is a raw WebSocket feed; GraphQL and Socket.IO need their own adapters.
import { websocket } from "spinetab/websocket";
export const queueSource = websocket<{ open: number }>("/ws/queue", { decoder: "json",});Your endpoint streams one JSON value per line (NDJSON). Only declare it repeatable when opening the request again is safe.
import { stream } from "spinetab/stream";
export const queueSource = stream<{ open: number }>("/api/queue/stream", { repeatable: true,});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.
4. Add a Client Component
Section titled “4. Add a Client Component”"use client";
import { queueSource } from "./queue-source";import { useLive } from "./live";
export function Queue() { const { data, error } = useLive(queueSource, { reconcile: "latest", });
if (error) return <p role="alert">Could not load the queue: {error.code}</p>; if (data === undefined) return <p role="status">Loading queue…</p>;
return <p>{data.open} open</p>;}The "use client" directive
puts this component and its imports, including live.ts, in the client module
graph. Creating the client starts no connection during server rendering. The
hook starts the subscription after the component mounts in the browser and
releases it when the component unmounts.
5. Render it from your page
Section titled “5. Render it from your page”import { Queue } from "./queue";
export default function Page() { return ( <main> <h1>Queue</h1> <Queue /> </main> );}Keep hooks and the live.ts import in Client Components. Server Components can
render Queue and pass it serialisable props; they cannot call Spinetab hooks.
Check the result
Section titled “Check the result”Open the page in two tabs of the same browser profile. With SharedWorker available, matching subscriptions share the upstream feed or polling schedule. Navigate away from the component in one tab: the other tab keeps receiving updates.
The hook renders the loading state during prerendering and initial hydration. If you chose polling, it starts in the browser, reads every five seconds by default and pauses when no consumer is eligible. See polling for the schedule and execution modes if the client falls back to running in each tab.
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.
Next steps
Section titled “Next steps”- Use GraphQL, Socket.IO or a client library with this setup.
- Add authentication in the client module. Keep server secrets out of browser code.
- Decide how your app recovers missed events.
Monorepo commands, basePath and CDN asset prefixes need additional configuration;
see Next.js configuration. The
binding reference covers server rendering and
client boundaries in detail.