Skip to content

Apollo Client recipes

Share GraphQL subscriptions through Apollo's client.

Connection
Open recipe

GraphQL over SSE · Astro · React

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-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.

Terminal window
pnpm add spinetab graphql@^17.0.2 graphql-sse@^2.6.1 @apollo/client@^4.3.1 rxjs@^7.8.2

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 react from "@astrojs/react";
import { defineConfig } from "astro/config";
import { spinetab } from "spinetab/astro";
export default defineConfig({ integrations: [react(), spinetab()] });

Save these files together in src/components/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.

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/endpoint.ts
import { graphqlSse } from "spinetab/graphql-sse";
export const endpoint = graphqlSse("/graphql/stream");
src/components/recipe/document.ts
import { gql, type TypedDocumentNode } from "@apollo/client";
import type { Queue } from "./queue-types";
export const QUEUE: TypedDocumentNode<{ queue: Queue }> = gql`
subscription Queue { queue { open } }
`;
src/components/recipe/client.ts
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",
}),
});
}
src/components/recipe/Queue.tsx
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>
);
}
src/components/recipe/Recipe.tsx
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>
);
}
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