Solid 2 guide
Use Sp00ky from a Solid 2.0 app: createQuery, <Loading> suspension, createSubmission, and the migration table from client-solid.
@spooky-sync/client-solid2 is the Solid 2.0 native binding. It is the counterpart of
@spooky-sync/client-solid, which stays on Solid 1.x — the two packages
coexist until Solid 2.0 is stable, and you pick one per app.
Solid 2.0 is still an RC. This package pins solid-js@^2.0.0-rc.0 and @solidjs/signals@^2.0.0-rc.0
as peers, and its semantics probes (src/lib/__tests__/rc-semantics.test.ts) exist to catch RC
drift. On Solid 1.x apps use the SolidJS guide instead.
What “native” means here
- Query results are a
createProjectionfed by an async generator over the engine’s live subscription: keyed reconcile byid, row identity preserved across updates,<For>notified on add / remove / reorder. No manual reconcile or version-signal plumbing. createQueryexposes both worlds: non-suspending accessors (data,isLoading,isFetching,isAuthoritative,hasData,isEmpty,isSettled,error) and a suspendingready()for<Loading>boundaries. Queries are born committed, so a local-first cache paint never suspends.- Status hooks are async-iterable-backed memos with a
loadingValue. - Mutations stay plain async calls — the engine is already optimistic local-first end to end
(local commit → live re-emit → outbox sync).
createSubmissiononly adds button pending/error state.
- InstallCoordinated RC
solid-js,@solidjs/weband@solidjs/signalsmust sit on matching2.0.0-rc.xversions. Pin them exactly. In Solid 2 the JSX import source is@solidjs/web.tsconfig.json - Generate types
Same generator as every other client — see Generated types.
- Configure the clientsrc/db.ts
Every key is documented in Client config.
- Mount the providersrc/App.tsx
Props are identical to client-solid’s provider:
config,fallback,preload(awaited gate before children render),onReady,onError. A mounted client is deliberately not closed on unmount.
Queries
createQuery replaces useQuery (kept as a deprecated alias). Same overloads: (query, options?)
with a provider in context, or (db, query, options?).
Pass a thunk when the query depends on signals — read them inside it:
Suspension style
ready() reads the same data but participates in Solid 2’s boundary protocol: it pends until the
first real result lands. <Loading> is Solid 2’s renamed <Suspense>.
Result surface
| Accessor | Meaning |
|---|---|
data() | Rows, or the row / null for .one(). Never suspends, never throws. Keyed-reconciled in place. |
ready() | Same data, suspends into the nearest <Loading> until the first result or an error. |
error() | Registration or sync error (e.g. SSP 503 during bootstrap). Never thrown into the render tree; the sync scheduler retries underneath. |
isLoading() | Cold first load only: nothing has painted for this identity, the server has not answered, and registration has not failed. A paint from the local cache ends it without the server; the server answering with zero rows ends it without rows. Re-arms only when the query identity changes. |
isFetching() | The sync engine is pulling records for this query (registration, a sync round). Re-enters on later rounds. |
isAuthoritative() | Server membership is known for this identity: a registration or poll landed, or a previous session’s membership was read on boot. From here data() is the server’s answer, not a cache guess. Latched until the identity changes. |
hasData() | data() holds something: rows for a list, a row for .one(). |
isEmpty() | Authoritative and no data: the server said “no rows”. Render empty states on this, never on a bare !hasData(). |
isSettled() | Authoritative and idle — the gate a windowed list needs before trusting a short result as the end of the list. Not monotonic (a later sync round re-enters isFetching); latch it when a verdict must stick. |
A query that painted cached rows is therefore not loading while the server is still catching
up, and a list that is merely waiting for the server is not empty. Gates built on
isLoading() / isEmpty() stop guessing with timeouts.
Options: { enabled?: () => boolean, deregisterOnCleanup?: boolean }, as in client-solid.
Solid 2 stores wrap class instances too and serve their methods bound. A RecordId read out of a
row renders fine and db.delete('post', row.id) works, but if you hand a whole row back to a
surrealdb API that checks instanceof, unwrap it first with snapshot(row) from solid-js.
Mutations
Engine writes are already optimistic and local-first, so mutations are plain calls:
For button pending/error state, wrap the call in createSubmission:
To track a backend job, query its outbox row: db.run() writes one with status: 'pending'.
Status hooks
Same shapes as client-solid, rebuilt on async-iterable-backed memos: useDb,
usePendingMutations, useSyncStatus, useSyncActivity,
useStorageStatus, useFeatureFlag,
useAppRelease, useCrdtField,
useFileUpload / useDownloadFile, and createPreload.
Migrating from client-solid
| client-solid (Solid 1) | client-solid2 (Solid 2) |
|---|---|
useQuery(q) | createQuery(q) (useQuery kept as an alias) |
data() may be undefined before the first fetch | data() is [] / null before the first fetch |
| No suspension | ready() + <Loading> |
<Suspense> | <Loading> |
jsxImportSource: "solid-js" | jsxImportSource: "@solidjs/web" |
| everything else | unchanged call signatures |
Testing
Under Node, solid-js resolves its node export condition to the SSR build, where user effects
never run. Point Vitest at the browser build: