Skip to content

Commit 5a4645d

Browse files
MAST1999cloutiertylerclockwork-labs-bot
authored
Add SolidJS integration with docs quickstart (#5052)
# Description of Changes This PR adds support for Solid. Related issue: #4820 The initial implementation was with an LLM, since Solid and React are very similar. Then I improved on it. The docs though are completely written by the LLM, I just read through them once to make sure there aren't any problems. # API and ABI breaking changes There are no API or ABI changes. Just added support for Solid. # Expected complexity level and risk I'd say around 1 or 2. I haven't tested it too much except for the example that I added. # Testing I'll be testing it more with the test app I'm planing to build. - [ ] I've added the test solid router similar to test react router, and it was working, but it'll be nice if someone else can verify as well. - [ ] I'll be testing it more with the test app I'm planing to build --------- Co-authored-by: Tyler Cloutier <cloutiertyler@users.noreply.github.com> Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com>
1 parent 16c74ee commit 5a4645d

67 files changed

Lines changed: 5501 additions & 474 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ members = [
6969
"tools/xtask-llm-benchmark",
7070
"crates/bindings-typescript/test-app/server",
7171
"crates/bindings-typescript/test-react-router-app/server",
72+
"crates/bindings-typescript/test-solid-router/server",
7273
"crates/query-builder",
7374
]
7475
default-members = ["crates/cli", "crates/standalone", "crates/update"]

crates/bindings-typescript/README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,97 @@ function App() {
111111
}
112112
```
113113

114+
#### SolidJS Usage
115+
116+
This module also includes SolidJS primitives to subscribe to tables under the `spacetimedb/solid` subpath. The SolidJS integration uses Solid's fine-grained reactivity system (`createSignal`, `createStore`, `createMemo`, `createComputed`) for optimal rendering performance. Reactive updates are scoped to only the data that actually changed.
117+
118+
In order to use SpacetimeDB SolidJS primitives in your project, first add a `SpacetimeDBProvider` at the top of your component hierarchy:
119+
120+
```tsx
121+
import { SpacetimeDBProvider } from 'spacetimedb/solid';
122+
import { DbConnection, tables } from './module_bindings';
123+
124+
const connectionBuilder = DbConnection.builder()
125+
.withUri('ws://localhost:3000')
126+
.withDatabaseName('MODULE_NAME')
127+
.withLightMode(true)
128+
.onDisconnect(() => {
129+
console.log('disconnected');
130+
})
131+
.onConnectError(() => {
132+
console.log('client_error');
133+
})
134+
.onConnect((conn, identity, _token) => {
135+
console.log(
136+
'Connected to SpacetimeDB with identity:',
137+
identity.toHexString()
138+
);
139+
140+
conn.subscriptionBuilder().subscribe(tables.player);
141+
})
142+
.withToken('TOKEN');
143+
144+
render(
145+
() => (
146+
<SpacetimeDBProvider connectionBuilder={connectionBuilder}>
147+
<App />
148+
</SpacetimeDBProvider>
149+
),
150+
document.getElementById('root')!
151+
);
152+
```
153+
154+
Once you add a `SpacetimeDBProvider` to your hierarchy, you can use the SpacetimeDB SolidJS primitives in your components:
155+
156+
```tsx
157+
import {
158+
useSpacetimeDB,
159+
useTable,
160+
useReducer,
161+
useProcedure,
162+
} from 'spacetimedb/solid';
163+
164+
function App() {
165+
// Access the connection state (identity, token, connection error, etc.)
166+
const conn = useSpacetimeDB();
167+
168+
// Subscribe to a table — returns a reactive store of rows and an isReady accessor
169+
const [rows, isReady] = useTable(() => tables.message);
170+
171+
// Subscribe to a filtered view
172+
const [onlineUsers, onlineReady] = useTable(
173+
() => tables.user.where(r => r.online.eq(true)),
174+
{
175+
onInsert: row => console.log('User came online:', row),
176+
onDelete: row => console.log('User went offline:', row),
177+
}
178+
);
179+
180+
// Call a reducer — queues calls made before the connection is ready
181+
const sendMessage = useReducer(reducers.sendMessage);
182+
183+
// Call a procedure — queues calls made before the connection is ready
184+
const getResult = useProcedure(procedures.getSomeResult);
185+
186+
return (
187+
<div>
188+
<Show when={isReady()} fallback={<p>Loading...</p>}>
189+
<p>{rows.length} messages</p>
190+
<For each={rows}>{row => <div>{row.text}</div>}</For>
191+
</Show>
192+
<button onClick={() => sendMessage('hello')}>Send</button>
193+
</div>
194+
);
195+
}
196+
```
197+
198+
**Key differences from the React API:**
199+
200+
- `useTable` takes a _getter function_ `() => Query<TableDef>` instead of a plain value, so the query can be reactive and update when signals change.
201+
- `useTable` returns `[rows, isReady]` where `rows` is a Solid reactive store and `isReady` is an accessor function `() => boolean`.
202+
- The `enabled` callback option is a getter `() => boolean` instead of a plain boolean, allowing it to depend on reactive state.
203+
- `useReducer` and `useProcedure` queue calls made before the connection is ready and flush them once connected.
204+
114205
### Developer notes
115206

116207
To run the tests, do:

crates/bindings-typescript/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@
9595
"import": "./dist/angular/index.mjs",
9696
"require": "./dist/angular/index.cjs",
9797
"default": "./dist/angular/index.mjs"
98+
},
99+
"./solid": {
100+
"types": "./dist/solid/index.d.ts",
101+
"import": "./dist/solid/index.mjs",
102+
"require": "./dist/solid/index.cjs",
103+
"default": "./dist/solid/index.mjs"
98104
}
99105
},
100106
"size-limit": [
@@ -189,6 +195,7 @@
189195
"@angular/core": ">=17.0.0",
190196
"@tanstack/react-query": "^5.0.0",
191197
"react": "^18.0.0 || ^19.0.0-0 || ^19.0.0",
198+
"solid-js": "^1.6.0",
192199
"svelte": "^4.0.0 || ^5.0.0",
193200
"undici": "^6.19.2",
194201
"vue": "^3.3.0"
@@ -200,6 +207,9 @@
200207
"react": {
201208
"optional": true
202209
},
210+
"solid-js": {
211+
"optional": true
212+
},
203213
"svelte": {
204214
"optional": true
205215
},
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import {
2+
DbConnectionBuilder,
3+
type DbConnectionImpl,
4+
} from '../sdk/db_connection_impl';
5+
import { onCleanup, createMemo, createComputed } from 'solid-js';
6+
import { createStore } from 'solid-js/store';
7+
import { SpacetimeDBContext } from './useSpacetimeDB';
8+
import type { ConnectionState } from './connection_state';
9+
import { ConnectionId } from '../lib/connection_id';
10+
import {
11+
ConnectionManager,
12+
type ConnectionState as ManagerConnectionState,
13+
} from '../sdk/connection_manager';
14+
15+
export interface SpacetimeDBProviderProps<
16+
DbConnection extends DbConnectionImpl<any>,
17+
> {
18+
connectionBuilder: DbConnectionBuilder<DbConnection>;
19+
children?: any;
20+
}
21+
22+
export function SpacetimeDBProvider<DbConnection extends DbConnectionImpl<any>>(
23+
props: SpacetimeDBProviderProps<DbConnection>
24+
) {
25+
const uri = () => props.connectionBuilder.getUri();
26+
const moduleName = () => props.connectionBuilder.getModuleName();
27+
28+
const key = createMemo(() => ConnectionManager.getKey(uri(), moduleName()));
29+
30+
const fallbackState: ManagerConnectionState = {
31+
isActive: false,
32+
identity: undefined,
33+
token: undefined,
34+
connectionId: ConnectionId.random(),
35+
connectionError: undefined,
36+
};
37+
38+
const [state, setState] = createStore<ManagerConnectionState>(fallbackState);
39+
40+
// Subscribe to ConnectionManager state changes
41+
createComputed(() => {
42+
const currentKey = key();
43+
44+
const unsubscribe = ConnectionManager.subscribe(currentKey, () => {
45+
const snapshot =
46+
ConnectionManager.getSnapshot(currentKey) ?? fallbackState;
47+
setState(snapshot);
48+
});
49+
50+
// Load initial snapshot
51+
const snapshot = ConnectionManager.getSnapshot(currentKey) ?? fallbackState;
52+
setState(snapshot);
53+
54+
onCleanup(() => {
55+
unsubscribe();
56+
});
57+
});
58+
59+
const getConnection = () =>
60+
ConnectionManager.getConnection<DbConnection>(key());
61+
62+
const contextValue: ConnectionState = {
63+
get isActive() {
64+
return state.isActive;
65+
},
66+
get identity() {
67+
return state.identity;
68+
},
69+
get token() {
70+
return state.token;
71+
},
72+
get connectionId() {
73+
return state.connectionId;
74+
},
75+
get connectionError() {
76+
return state.connectionError;
77+
},
78+
getConnection,
79+
};
80+
81+
// Retain / release lifecycle
82+
createComputed(() => {
83+
const currentKey = key();
84+
ConnectionManager.retain(currentKey, props.connectionBuilder);
85+
86+
onCleanup(() => {
87+
ConnectionManager.release(currentKey);
88+
});
89+
});
90+
91+
return SpacetimeDBContext.Provider({
92+
value: contextValue,
93+
get children() {
94+
return props.children;
95+
},
96+
});
97+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { DbConnectionImpl } from '../sdk/db_connection_impl';
2+
import type { ConnectionState as ManagerConnectionState } from '../sdk/connection_manager';
3+
4+
export type ConnectionState = ManagerConnectionState & {
5+
getConnection(): DbConnectionImpl<any> | null;
6+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from './SpacetimeDBProvider.ts';
2+
export { useSpacetimeDB } from './useSpacetimeDB.ts';
3+
export { useTable } from './useTable.ts';
4+
export { useReducer } from './useReducer.ts';
5+
export { useProcedure } from './useProcedure.ts';
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { createEffect } from 'solid-js';
2+
import type { UntypedProcedureDef } from '../sdk/procedures';
3+
import { useSpacetimeDB } from './useSpacetimeDB';
4+
import type {
5+
ProcedureParamsType,
6+
ProcedureReturnType,
7+
} from '../sdk/type_utils';
8+
9+
export function useProcedure<ProcedureDef extends UntypedProcedureDef>(
10+
procedureDef: ProcedureDef
11+
): (
12+
...params: ProcedureParamsType<ProcedureDef>
13+
) => Promise<ProcedureReturnType<ProcedureDef>> {
14+
const { getConnection, isActive } = useSpacetimeDB();
15+
const procedureName = procedureDef.accessorName;
16+
17+
// Holds calls made before the connection exists
18+
const queue: {
19+
params: ProcedureParamsType<ProcedureDef>;
20+
resolve: (val: any) => void;
21+
reject: (err: unknown) => void;
22+
}[] = [];
23+
24+
// Flush when we finally have a connection
25+
createEffect(() => {
26+
if (!isActive) return;
27+
28+
const conn = getConnection();
29+
if (!conn) return;
30+
31+
const fn = (conn.procedures as any)[procedureName] as (
32+
...p: ProcedureParamsType<ProcedureDef>
33+
) => Promise<ProcedureReturnType<ProcedureDef>>;
34+
35+
if (queue.length) {
36+
const pending = queue.splice(0);
37+
for (const item of pending) {
38+
fn(...item.params).then(item.resolve, item.reject);
39+
}
40+
}
41+
});
42+
43+
return (...params: ProcedureParamsType<ProcedureDef>) => {
44+
const conn = getConnection();
45+
if (!conn) {
46+
return new Promise<ProcedureReturnType<ProcedureDef>>(
47+
(resolve, reject) => {
48+
queue.push({ params, resolve, reject });
49+
}
50+
);
51+
}
52+
const fn = (conn.procedures as any)[procedureName] as (
53+
...p: ProcedureParamsType<ProcedureDef>
54+
) => Promise<ProcedureReturnType<ProcedureDef>>;
55+
return fn(...params);
56+
};
57+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { createEffect } from 'solid-js';
2+
import type { UntypedReducerDef } from '../sdk/reducers';
3+
import { useSpacetimeDB } from './useSpacetimeDB';
4+
import type { ParamsType } from '../sdk';
5+
6+
export function useReducer<ReducerDef extends UntypedReducerDef>(
7+
reducerDef: ReducerDef
8+
): (...params: ParamsType<ReducerDef>) => Promise<void> {
9+
const { getConnection, isActive } = useSpacetimeDB();
10+
const reducerName = reducerDef.accessorName;
11+
12+
// Holds calls made before the connection exists
13+
const queue: {
14+
params: ParamsType<ReducerDef>;
15+
resolve: () => void;
16+
reject: (err: unknown) => void;
17+
}[] = [];
18+
19+
// Flush when we finally have a connection
20+
createEffect(() => {
21+
if (!isActive) return;
22+
23+
const conn = getConnection();
24+
if (!conn) return;
25+
26+
const fn = (conn.reducers as any)[reducerName] as (
27+
...p: ParamsType<ReducerDef>
28+
) => Promise<void>;
29+
30+
if (queue.length) {
31+
const pending = queue.splice(0);
32+
for (const item of pending) {
33+
fn(...item.params).then(item.resolve, item.reject);
34+
}
35+
}
36+
});
37+
38+
return (...params: ParamsType<ReducerDef>) => {
39+
const conn = getConnection();
40+
if (!conn) {
41+
return new Promise<void>((resolve, reject) => {
42+
queue.push({ params, resolve, reject });
43+
});
44+
}
45+
const fn = (conn.reducers as any)[reducerName] as (
46+
...p: ParamsType<ReducerDef>
47+
) => Promise<void>;
48+
return fn(...params);
49+
};
50+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { createContext, useContext } from 'solid-js';
2+
import type { ConnectionState } from './connection_state';
3+
4+
export const SpacetimeDBContext = createContext<ConnectionState | undefined>(
5+
undefined
6+
);
7+
8+
// Throws an error if used outside of a SpacetimeDBProvider
9+
// Error is caught by other hooks like useTable so they can provide better error messages
10+
export function useSpacetimeDB(): ConnectionState {
11+
const context = useContext(SpacetimeDBContext) as ConnectionState | undefined;
12+
if (!context) {
13+
throw new Error(
14+
'useSpacetimeDB must be used within a SpacetimeDBProvider component. Did you forget to add a `SpacetimeDBProvider` to your component tree?'
15+
);
16+
}
17+
return context;
18+
}

0 commit comments

Comments
 (0)