Skip to content

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.

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

Pass your existing config to withSpinetab:

next.config.ts
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.

app/live.ts
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.

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/queue.tsx
"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.

app/page.tsx
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.

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.

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.