Skip to content

Direct subscriptions recipes

Render a live feed with your framework's component binding.

Open recipe

Socket.IO · Astro · Solid

Start with an existing Astro · Solid app. Keep its renderer, routes and plugins. These files add one live view; Spinetab does not create your API.

Your Socket.IO server emits socket.emit("queue", { open: 12 }) in the default namespace, using the default /socket.io path. Each event contains the complete queue. The example chooses sharing: "shared": use it only if merging tabs onto one socket suits your server's presence and membership rules.

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.

Terminal window
pnpm add spinetab socket.io-client@^4.8.4

Keep your framework's existing dependencies. See compatible versions if upgrading an older 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.

astro.config.mjs
import solid from "@astrojs/solid-js";
import { defineConfig } from "astro/config";
import { spinetab } from "spinetab/astro";
export default defineConfig({ integrations: [solid(), spinetab()] });

Save these files together in src/components/recipe/.

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.

src/components/recipe/live.ts
import { createSpinetab } from "spinetab";
export const spinetab = createSpinetab({ anonymous: true });
src/components/recipe/queue-types.ts
export type Queue = { open: number };
export type QueueView = { open?: number; problem?: string };
src/components/recipe/source.ts
import { socketIo } from "spinetab/socket-io";
import type { Queue, QueueView } from "./queue-types";
export const queueSource = socketIo("/", { sharing: "shared" }).subscription<
[Queue]
>({ event: "queue" });
export const selectQueue = ([queue]: [Queue]): QueueView => queue;
src/components/recipe/Recipe.tsx
import { bindClient } from "spinetab/solid";
import { spinetab } from "./live";
import { queueSource, selectQueue } from "./source";
const { createLive } = bindClient(spinetab);
export default function Recipe() {
const queue = createLive(queueSource, {
map: selectQueue,
reconcile: "latest",
});
const problem = () => queue.error()?.message ?? queue.data()?.problem;
return (
<>
{problem() ? (
<p role="alert">{problem()}</p>
) : queue.data()?.open === undefined ? (
<p role="status">Loading queue…</p>
) : (
<p>
<output>{queue.data()?.open}</output> open
</p>
)}
</>
);
}
src/pages/queue.astro
---
import Recipe from "../components/recipe/Recipe";
---
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Live queue</title>
</head>
<body>
<Recipe client:load />
</body>
</html>

Provider and consumer live in this one hydrated island. client:load starts it on page load; client:visible defers it until visible. Without hydration no subscription starts.

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.

API options and advanced recovery · Authentication