SWR recipes
Use shared subscriptions in a React app with SWR.
GraphQL over WebSocket · Astro · React
Before you start
Section titled “Before you start”Start with an existing Astro · React app. Keep its renderer, routes and plugins. These files add one live view; Spinetab does not create your API.
Your schema exposes Subscription.queue: Queue! with Queue.open: Int!. Each event contains the complete queue value. Serve the graphql-transport-ws protocol at /graphql. Queries and mutations, if used, go to /graphql. This is a GraphQL endpoint, not a plain SSE or WebSocket feed.
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 graphql@^17.0.2 graphql-ws@^6.3.0 swr@^2.5.1Keep 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 react from "@astrojs/react";import { defineConfig } from "astro/config";import { spinetab } from "spinetab/astro";
export default defineConfig({ integrations: [react(), spinetab()] });3. Connect and render
Section titled “3. Connect and render”Save these files together in src/components/recipe/.
SWR uses its default cache. The constant queue key always returns the same source; if the endpoint varies, derive it from that key. With SWR 2.5.1 and React 19.3, avoid nesting a custom cache provider inside StrictMode; see the upstream restriction.
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 { parse } from "graphql";import { type GraphqlDocument, type GraphqlResult, graphqlWs,} from "spinetab/graphql-ws";import type { Queue, QueueView } from "./queue-types";
const document: GraphqlDocument< { queue: Queue }, Record<string, never>> = parse("subscription Queue { queue { open } }");
export const queueSource = graphqlWs("/graphql").subscription({ query: document,});
export function selectQueue( result: GraphqlResult<{ queue: Queue }>,): QueueView { return { open: result.data?.queue?.open, problem: result.errors?.map((error) => error.message).join("; ") || (result.data?.queue ? undefined : "The server returned no queue."), };}import { swrSubscription } from "spinetab/swr";import useSWRSubscription from "swr/subscription";import { spinetab } from "./live";import { queueSource, selectQueue } from "./source";
const subscribe = swrSubscription(spinetab, (_key: "queue") => queueSource, { map: selectQueue, reconcile: "latest",});
export default function Recipe() { const { data, error } = useSWRSubscription("queue", subscribe); if (error || data?.problem) return <p role="alert">{error?.message ?? data?.problem}</p>; if (data?.open === undefined) return <p role="status">Loading queue…</p>; return ( <p> <output>{data.open}</output> open </p> );}4. Mount the view
Section titled “4. Mount the view”---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.
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.