Apollo Client recipes
Share GraphQL subscriptions through Apollo's client.
GraphQL over SSE · React Router
Before you start
Section titled “Before you start”Start with an existing React Router 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-sse distinct protocol at /graphql/stream. 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-sse@^2.6.1 @apollo/client@^4.3.1 rxjs@^7.8.2Keep 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 { 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}"] },});3. Connect and render
Section titled “3. Connect and render”Save these files together in app/recipe/.
The provider supplies Apollo's client to its subscription hook. Keep an existing app provider/client when extending an Apollo app rather than introducing a second cache.
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 { graphqlSse } from "spinetab/graphql-sse";
export const endpoint = graphqlSse("/graphql/stream");import { gql, type TypedDocumentNode } from "@apollo/client";import type { Queue } from "./queue-types";
export const QUEUE: TypedDocumentNode<{ queue: Queue }> = gql` subscription Queue { queue { open } }`;import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";import { spinetabSplit } from "spinetab/apollo";import { endpoint } from "./endpoint";import { spinetab } from "./live";
export function makeClient() { return new ApolloClient({ cache: new InMemoryCache(), link: spinetabSplit(spinetab, endpoint, new HttpLink({ uri: "/graphql" }), { reconcile: "latest", }), });}import { useSubscription } from "@apollo/client/react";import { QUEUE } from "./document";
export default function Queue() { const { data, error } = useSubscription(QUEUE); if (error) return <p role="alert">{error.message}</p>; if (!data) return <p role="status">Loading queue…</p>; return ( <p> <output>{data.queue.open}</output> open </p> );}import { ApolloProvider } from "@apollo/client/react";import { useState } from "react";import { makeClient } from "./client";import Queue from "./Queue";
export default function Recipe() { const [client] = useState(makeClient); return ( <ApolloProvider client={client}> <Queue /> </ApolloProvider> );}4. Mount the view
Section titled “4. Mount the view”import Recipe from "../recipe/Recipe";export default function QueuePage() { return ( <main> <Recipe /> </main> );}import { type RouteConfig, route } from "@react-router/dev/routes";
export default [route("queue", "routes/queue.tsx")] satisfies RouteConfig;Keep your existing routes and root Outlet. The dependency-scan entries include route imports on a cold first visit. Declarative/Data Mode apps use the Vite recipe instead.
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.