Skip to content

Commit af732f5

Browse files
feat(tanstack): add SSR prefetching for Tanstack Start (#4519)
# Description of Changes - Add SSR prefetching for Tanstack Start Closes #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
1 parent 33f3ea1 commit af732f5

4 files changed

Lines changed: 89 additions & 0 deletions

File tree

crates/bindings-typescript/src/tanstack/SpacetimeDBQueryClient.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export class SpacetimeDBQueryClient {
9696
setConnection(connection: SpacetimeConnection): void {
9797
this.connection = connection;
9898
this.processPendingQueries();
99+
this.hydrateSubscriptions();
99100
}
100101

101102
connect(queryClient: QueryClient): void {
@@ -289,6 +290,29 @@ export class SpacetimeDBQueryClient {
289290
}
290291
}
291292

293+
// subscribe to queries with SSR cached data but no active subscription
294+
private hydrateSubscriptions(): void {
295+
if (!this.connection || !this.queryClient) {
296+
return;
297+
}
298+
299+
for (const [querySql, { accessorName, whereExpr }] of queryRegistry) {
300+
const queryKey = ['spacetimedb', accessorName, querySql] as const;
301+
const keyStr = JSON.stringify(queryKey);
302+
303+
if (this.subscriptions.has(keyStr)) {
304+
continue;
305+
}
306+
if (this.queryClient.getQueryData(queryKey) === undefined) {
307+
continue;
308+
}
309+
310+
this.setupSubscription(queryKey, accessorName, querySql, whereExpr).catch(
311+
() => {}
312+
);
313+
}
314+
}
315+
292316
// clean up all subscriptions and disconnect
293317
disconnect(): void {
294318
if (this.cacheUnsubscribe) {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { DbConnection, tables } from '../module_bindings';
2+
import { Person } from '../module_bindings/types';
3+
import type { Infer } from 'spacetimedb';
4+
5+
const HOST = process.env.SPACETIMEDB_HOST ?? 'ws://localhost:3000';
6+
const DB_NAME = process.env.SPACETIMEDB_DB_NAME ?? 'tanstack-ts';
7+
8+
export type PersonData = Infer<typeof Person>;
9+
10+
export async function fetchPeople(): Promise<PersonData[]> {
11+
return new Promise((resolve, reject) => {
12+
const timeoutId = setTimeout(() => {
13+
reject(new Error('SpacetimeDB connection timeout'));
14+
}, 10000);
15+
16+
DbConnection.builder()
17+
.withUri(HOST)
18+
.withDatabaseName(DB_NAME)
19+
.onConnect(conn => {
20+
// Subscribe to all people
21+
conn
22+
.subscriptionBuilder()
23+
.onApplied(() => {
24+
clearTimeout(timeoutId);
25+
// Get all people from the cache
26+
const people = Array.from(conn.db.person.iter());
27+
conn.disconnect();
28+
resolve(people);
29+
})
30+
.onError(ctx => {
31+
clearTimeout(timeoutId);
32+
conn.disconnect();
33+
reject(ctx.event ?? new Error('Subscription error'));
34+
})
35+
.subscribe(tables.person);
36+
})
37+
.onConnectError((_ctx, error) => {
38+
clearTimeout(timeoutId);
39+
reject(error);
40+
})
41+
.build();
42+
});
43+
}

templates/tanstack-ts/src/routes/index.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
11
import { createFileRoute } from '@tanstack/react-router';
2+
import { createServerFn } from '@tanstack/react-start';
23
import { useState } from 'react';
34
import { tables, reducers } from '../module_bindings';
45
import {
6+
spacetimeDBQuery,
57
useSpacetimeDB,
68
useReducer,
79
useSpacetimeDBQuery,
810
} from 'spacetimedb/tanstack';
11+
import { fetchPeople } from '../lib/spacetimedb-server';
12+
13+
const getPeople = createServerFn({ method: 'GET' }).handler(async () => {
14+
try {
15+
return await fetchPeople();
16+
} catch (error) {
17+
console.error('Failed to prefetch people:', error);
18+
return [];
19+
}
20+
});
921

1022
export const Route = createFileRoute('/')({
23+
loader: async ({ context }) => {
24+
// Seed React Query cache with server fetched data, fetches if cache is empty
25+
await context.queryClient.ensureQueryData({
26+
...spacetimeDBQuery(tables.person),
27+
queryFn: () => getPeople(),
28+
});
29+
},
1130
component: App,
1231
});
1332

templates/tanstack-ts/vite.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ export default defineConfig({
77
server: {
88
port: 5173,
99
},
10+
resolve: {
11+
dedupe: ['react', 'react-dom', '@tanstack/react-query'],
12+
},
1013
plugins: [
1114
tsConfigPaths({
1215
projects: ['./tsconfig.json'],

0 commit comments

Comments
 (0)