Skip to content

Commit b973b0a

Browse files
committed
feat: add react sdk
1 parent bd8d191 commit b973b0a

File tree

9 files changed

+203
-182
lines changed

9 files changed

+203
-182
lines changed

packages/sdk/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
"node": ">=18.0.0"
4040
},
4141
"peerDependencies": {
42-
"undici": "^6.19.2"
42+
"undici": "^6.19.2",
43+
"react": ">=17.0.0"
4344
},
4445
"devDependencies": {
4546
"@clockworklabs/test-app": "file:../test-app",
@@ -49,5 +50,10 @@
4950
"dependencies": {
5051
"@zxing/text-encoding": "^0.9.0",
5152
"base64-js": "^1.5.1"
53+
},
54+
"peerDependenciesMeta": {
55+
"react": {
56+
"optional": true
57+
}
5258
}
5359
}

packages/sdk/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ export * from './identity.ts';
88
export * from './message_types.ts';
99
export * from './timestamp.ts';
1010
export * from './time_duration.ts';
11+
12+
export * from './react';
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import {
2+
useEffect,
3+
useMemo,
4+
useRef,
5+
useState,
6+
type FC,
7+
type ReactNode,
8+
} from 'react';
9+
import { SpacetimeDBContext } from './useSpacetimeDB';
10+
import type { DbConnectionBuilder } from '@clockworklabs/spacetimedb-sdk';
11+
12+
export type ConnectionStatus =
13+
| 'idle'
14+
| 'connecting'
15+
| 'connected'
16+
| 'disconnected'
17+
| 'error';
18+
19+
export const SpacetimeDBProvider: FC<{
20+
builder: DbConnectionBuilder<any, any, any>;
21+
moduleName: string;
22+
uri: string;
23+
compression?: 'gzip' | 'none';
24+
lightMode?: boolean;
25+
children?: ReactNode;
26+
fallback?: ReactNode;
27+
}> = ({
28+
builder,
29+
moduleName,
30+
uri,
31+
compression,
32+
lightMode,
33+
children,
34+
fallback,
35+
}) => {
36+
const [client, setClient] = useState<any>(null);
37+
const [status, setStatus] = useState<ConnectionStatus>('idle');
38+
const aliveRef = useRef(true); // Prevent issues with late events after unmount, especially with StrictMode
39+
40+
const configuredBuilder = useMemo(() => {
41+
return builder
42+
.withModuleName(moduleName)
43+
.withUri(uri)
44+
.withCompression(compression ?? 'gzip')
45+
.withLightMode(lightMode ?? false);
46+
}, [builder, moduleName, uri, compression, lightMode]);
47+
48+
useEffect(() => {
49+
aliveRef.current = true;
50+
setStatus('connecting');
51+
52+
const b = configuredBuilder
53+
.onConnect(() => {
54+
if (!aliveRef.current) return;
55+
setStatus('connected');
56+
})
57+
.onDisconnect(() => {
58+
if (!aliveRef.current) return;
59+
setStatus('disconnected');
60+
})
61+
.onConnectError(() => {
62+
if (!aliveRef.current) return;
63+
setStatus('error');
64+
});
65+
66+
const newClient = b.build();
67+
setClient((prev: any) => {
68+
prev?.disconnect?.();
69+
return newClient;
70+
});
71+
72+
return () => {
73+
aliveRef.current = false;
74+
newClient?.disconnect?.();
75+
};
76+
}, [configuredBuilder]);
77+
78+
if (!client || status !== 'connected') {
79+
return <>{fallback ?? <div>Connecting to SpacetimeDB…</div>}</>;
80+
}
81+
82+
return (
83+
<SpacetimeDBContext.Provider
84+
value={{ client, status, identity: client.identity, token: client.token }}
85+
>
86+
{children}
87+
</SpacetimeDBContext.Provider>
88+
);
89+
};

packages/sdk/src/react/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './SpacetimeDBProvider.tsx';
2+
export { useSpacetimeDB } from './useSpacetimeDB.ts';
3+
export { useSubscription } from './useSubscription.ts';
4+
export { useTable } from './useTable.ts';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createContext, useContext } from "react";
2+
import type { ConnectionStatus } from "./SpacetimeDBProvider";
3+
import type { Identity } from "@clockworklabs/spacetimedb-sdk";
4+
5+
export const SpacetimeDBContext = createContext<any>(null);
6+
7+
export function useSpacetimeDB<T>(): { status: ConnectionStatus; client: T; identity: Identity; token: string } {
8+
return useContext(SpacetimeDBContext);
9+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useEffect, useMemo, useRef, useCallback } from "react";
2+
import type { DbConnection } from "../module_bindings";
3+
import { useSpacetimeDB } from "./useSpacetimeDB";
4+
5+
type UseSubscriptionCallbacks = {
6+
onApplied?: () => void;
7+
onError?: (error: Error) => void;
8+
onUnsubscribed?: () => void;
9+
};
10+
11+
export function useSubscription(query: string, callbacks?: UseSubscriptionCallbacks) {
12+
const { client } = useSpacetimeDB<DbConnection>();
13+
14+
const cbRef = useRef<UseSubscriptionCallbacks | undefined>(callbacks);
15+
cbRef.current = callbacks;
16+
17+
const cancelRef = useRef<(() => Promise<void> | void) | null>(null);
18+
19+
const cancel = useCallback(() => {
20+
return cancelRef.current?.();
21+
}, []);
22+
23+
useEffect(() => {
24+
const subscription = client
25+
.subscriptionBuilder()
26+
.onApplied(() => cbRef.current?.onApplied?.())
27+
.onError((e) => cbRef.current?.onError?.(e as any))
28+
.subscribe(query);
29+
30+
cancelRef.current = () => {
31+
subscription.unsubscribe();
32+
};
33+
34+
return () => {
35+
const fn = cancelRef.current;
36+
cancelRef.current = null;
37+
fn?.();
38+
};
39+
}, [query, client]);
40+
41+
return useMemo(() => ({ cancel }), [cancel]);
42+
}

packages/sdk/src/react/useTable.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { useEffect, useState } from "react";
2+
3+
export interface TableCallbacks<TRow> {
4+
onInsert?: (row: TRow) => void;
5+
onDelete?: (row: TRow) => void;
6+
onUpdate?: (oldRow: TRow, newRow: TRow) => void;
7+
}
8+
9+
export function useTable<TRow>(table: any, callbacks?: TableCallbacks<TRow>): TRow[] {
10+
const [rows, setRows] = useState<TRow[]>([]);
11+
12+
useEffect(() => {
13+
table.onInsert((_: any, row: any) => {
14+
setRows(table.iter() as TRow[]);
15+
if (callbacks?.onInsert) {
16+
callbacks.onInsert(row);
17+
}
18+
});
19+
20+
table.onDelete((_: any, row: any) => {
21+
setRows(table.iter() as TRow[]);
22+
if (callbacks?.onDelete) {
23+
callbacks.onDelete(row);
24+
}
25+
});
26+
27+
if (table.onUpdate) {
28+
table.onUpdate((_: any, oldRow: any, newRow: any) => {
29+
setRows(table.iter() as TRow[]);
30+
if (callbacks?.onUpdate) {
31+
callbacks.onUpdate(oldRow, newRow);
32+
}
33+
});
34+
}
35+
36+
return () => {};
37+
}, [table, callbacks]);
38+
39+
return rows;
40+
}
41+
42+
// TODO:
43+
// Add a hook for reducers

packages/sdk/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"moduleResolution": "Bundler",
1616
"allowSyntheticDefaultImports": true,
1717
"isolatedDeclarations": true,
18-
"isolatedModules": true
18+
"isolatedModules": true,
19+
"jsx": "react-jsx"
1920
},
2021
"include": ["src/**/*"],
2122
"exclude": ["node_modules", "**/__tests__/*", "dist/**/*"]

0 commit comments

Comments
 (0)