feat(tanstack): add SSR prefetching for Tanstack Start (#4519)

# Description of Changes
- Add SSR prefetching for Tanstack Start

Closes https://github.com/clockworklabs/SpacetimeDB/issues/4438

<!-- Please describe your change, mention any related tickets, and so on
here. -->

# API and ABI breaking changes

<!-- If this is an API or ABI breaking change, please apply the
corresponding GitHub label. -->

# Expected complexity level and risk
1

<!--
How complicated do you think these changes are? Grade on a scale from 1
to 5,
where 1 is a trivial change, and 5 is a deep-reaching and complex
change.

This complexity rating applies not only to the complexity apparent in
the diff,
but also to its interactions with existing and future code.

If you answered more than a 2, explain what is complex about the PR,
and what other components it interacts with in potentially concerning
ways. -->

# Testing

<!-- Describe any testing you've done, and any testing you'd like your
reviewers to do,
so that you're confident that all the changes work as expected! -->

- [x] SSR prefetch works from testing
This commit is contained in:
clockwork-tien
2026-03-03 17:09:07 +02:00
committed by GitHub
parent 33f3ea18e2
commit af732f5ede
4 changed files with 89 additions and 0 deletions
@@ -96,6 +96,7 @@ export class SpacetimeDBQueryClient {
setConnection(connection: SpacetimeConnection): void {
this.connection = connection;
this.processPendingQueries();
this.hydrateSubscriptions();
}
connect(queryClient: QueryClient): void {
@@ -289,6 +290,29 @@ export class SpacetimeDBQueryClient {
}
}
// subscribe to queries with SSR cached data but no active subscription
private hydrateSubscriptions(): void {
if (!this.connection || !this.queryClient) {
return;
}
for (const [querySql, { accessorName, whereExpr }] of queryRegistry) {
const queryKey = ['spacetimedb', accessorName, querySql] as const;
const keyStr = JSON.stringify(queryKey);
if (this.subscriptions.has(keyStr)) {
continue;
}
if (this.queryClient.getQueryData(queryKey) === undefined) {
continue;
}
this.setupSubscription(queryKey, accessorName, querySql, whereExpr).catch(
() => {}
);
}
}
// clean up all subscriptions and disconnect
disconnect(): void {
if (this.cacheUnsubscribe) {
@@ -0,0 +1,43 @@
import { DbConnection, tables } from '../module_bindings';
import { Person } from '../module_bindings/types';
import type { Infer } from 'spacetimedb';
const HOST = process.env.SPACETIMEDB_HOST ?? 'ws://localhost:3000';
const DB_NAME = process.env.SPACETIMEDB_DB_NAME ?? 'tanstack-ts';
export type PersonData = Infer<typeof Person>;
export async function fetchPeople(): Promise<PersonData[]> {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('SpacetimeDB connection timeout'));
}, 10000);
DbConnection.builder()
.withUri(HOST)
.withDatabaseName(DB_NAME)
.onConnect(conn => {
// Subscribe to all people
conn
.subscriptionBuilder()
.onApplied(() => {
clearTimeout(timeoutId);
// Get all people from the cache
const people = Array.from(conn.db.person.iter());
conn.disconnect();
resolve(people);
})
.onError(ctx => {
clearTimeout(timeoutId);
conn.disconnect();
reject(ctx.event ?? new Error('Subscription error'));
})
.subscribe(tables.person);
})
.onConnectError((_ctx, error) => {
clearTimeout(timeoutId);
reject(error);
})
.build();
});
}
@@ -1,13 +1,32 @@
import { createFileRoute } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
import { useState } from 'react';
import { tables, reducers } from '../module_bindings';
import {
spacetimeDBQuery,
useSpacetimeDB,
useReducer,
useSpacetimeDBQuery,
} from 'spacetimedb/tanstack';
import { fetchPeople } from '../lib/spacetimedb-server';
const getPeople = createServerFn({ method: 'GET' }).handler(async () => {
try {
return await fetchPeople();
} catch (error) {
console.error('Failed to prefetch people:', error);
return [];
}
});
export const Route = createFileRoute('/')({
loader: async ({ context }) => {
// Seed React Query cache with server fetched data, fetches if cache is empty
await context.queryClient.ensureQueryData({
...spacetimeDBQuery(tables.person),
queryFn: () => getPeople(),
});
},
component: App,
});
+3
View File
@@ -7,6 +7,9 @@ export default defineConfig({
server: {
port: 5173,
},
resolve: {
dedupe: ['react', 'react-dom', '@tanstack/react-query'],
},
plugins: [
tsConfigPaths({
projects: ['./tsconfig.json'],