Skip to content

React Router

This guide uses React Router Framework Mode with its Vite plugin. For a standalone React app using Declarative or Data Mode, follow the Vite guide instead.

The examples display a queue value such as { "open": 12 }. Choose the source that matches your backend in step 3. Use an existing endpoint 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

Keep your existing plugins and add spinetab():

vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { spinetab } from "spinetab/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [reactRouter(), spinetab()],
optimizeDeps: { entries: ["app/**/*.{ts,tsx}"] },
});

The plugin generates the worker from your adapter imports. Keep any existing path-alias or styling plugins too, and restart the dev server. The entries setting includes route files in Vite’s initial dependency scan, avoiding a dependency reload on the first visit. Preserve any other entries your app already scans.

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

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/routes/queue.tsx
import { queueSource } from "../queue-source";
import { useLive } from "../live";
export default 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>;
}

Register it in your route config, keeping your existing routes:

app/routes.ts
import { type RouteConfig, route } from "@react-router/dev/routes";
export default [
// Keep your existing routes here.
route("queue", "routes/queue.tsx"),
] satisfies RouteConfig;

Your existing app/root.tsx should render an <Outlet /> for its child routes.

The hook renders the loading state during SSR and starts the subscription when the route component mounts in the browser. Unmounting releases it. This standard Framework Mode setup does not need a "use client" directive or disabled SSR. Use Spinetab in the component, not in a server loader or action.

Visit /queue in your running app. Navigate away in one tab: the subscription in the other tab continues.

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.

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.

See Vite framework configuration for build options and React bindings for hook APIs.