Overview
Zero provides Solid.js integration with reactive primitives that seamlessly integrate with Solid’s fine-grained reactivity system.
Installation
npm install @rocicorp/zero @rocicorp/zero-solid
Setup
ZeroProvider
Wrap your app with ZeroProvider to make Zero available to all components:
import { ZeroProvider } from '@rocicorp/zero-solid' ;
import { schema } from './schema' ;
function App () {
return (
< ZeroProvider
schema = { schema }
server = "https://your-zero-server.com"
userID = "user-123"
>
< YourApp />
</ ZeroProvider >
);
}
Provider Props
The ZeroProvider accepts the same props as the Zero constructor:
< ZeroProvider
schema = { schema }
server = "https://your-zero-server.com"
userID = "user-123"
auth = "your-auth-token"
mutators = { mutators }
context = { { sub: 'user-123' , role: 'admin' } }
logLevel = "info"
onOnlineChange = { ( online ) => console . log ( 'Online:' , online ) }
>
< YourApp />
</ ZeroProvider >
useQuery
The useQuery hook returns reactive accessors for query data:
import { useQuery } from '@rocicorp/zero-solid' ;
import { createBuilder } from '@rocicorp/zero' ;
import { schema } from './schema' ;
const zql = createBuilder ( schema );
function UserList () {
const [ users , resultDetails ] = useQuery (() => zql . user );
return (
< div >
< Show when = { resultDetails (). type === 'unknown' } >
< div > Loading... </ div >
</ Show >
< Show when = { resultDetails (). type === 'error' } >
< div > Error: { resultDetails (). error ?. message } </ div >
</ Show >
< Show when = { resultDetails (). type === 'complete' } >
< ul >
< For each = { users () } >
{ user => < li > { user . name } </ li > }
</ For >
</ ul >
</ Show >
</ div >
);
}
Query Result
useQuery returns a tuple of accessors:
const [ data , details ] = useQuery (() => query );
data(): Accessor returning the query result
details(): Accessor returning query metadata with type property:
unknown - Loading, data may be partial
complete - Fully loaded from server
error - Query failed with error details
Reactive Queries
Queries automatically track dependencies:
function FilteredUserList () {
const [ status , setStatus ] = createSignal < 'active' | 'inactive' >( 'active' );
// Query automatically updates when status changes
const [ users ] = useQuery (() =>
zql . user . where ( 'status' , status ())
);
return (
< div >
< select
value = { status () }
onChange = { e => setStatus ( e . target . value as 'active' | 'inactive' ) }
>
< option value = "active" > Active </ option >
< option value = "inactive" > Inactive </ option >
</ select >
< ul >
< For each = { users () } >
{ user => < li > { user . name } </ li > }
</ For >
</ ul >
</ div >
);
}
Conditional Queries
Pass falsy values to disable queries:
function UserProfile ( props : { userID : string | null }) {
// Query is disabled when userID is null
const [ user , details ] = useQuery (() =>
props . userID
? zql . user . where ( 'id' , props . userID ). one ()
: null
);
return (
< Show when = { props . userID } fallback = { < div > Select a user </ div > } >
< Show when = { details (). type === 'unknown' } >
< div > Loading... </ div >
</ Show >
< Show when = { user () } >
{ u => < div > { u (). name } </ div > }
</ Show >
</ Show >
);
}
Query Options
// Set TTL (time to live)
const [ users ] = useQuery (
() => zql . user ,
{ ttl: '5m' } // Keep alive for 5 minutes
);
// Or use reactive options
const [ ttl , setTTL ] = createSignal < TTL >( '1m' );
const [ users ] = useQuery (
() => zql . user ,
() => ({ ttl: ttl () })
);
useZero
Access the Zero instance:
import { useZero } from '@rocicorp/zero-solid' ;
function MyComponent () {
const zero = useZero ();
const handleMutation = async () => {
await zero (). mutate . user . insert ({
id: 'user-123' ,
name: 'Alice' ,
});
};
return < button onClick = { handleMutation } > Create User </ button > ;
}
useZero() returns an accessor that provides the Zero instance.
useConnectionState
Monitor the connection status:
import { useConnectionState } from '@rocicorp/zero-solid' ;
import { ConnectionStatus } from '@rocicorp/zero' ;
function ConnectionIndicator () {
const connectionState = useConnectionState ();
return (
< div >
Status: { connectionState (). status }
< Show when = { connectionState (). status === ConnectionStatus . Connected } >
< span >
Connected at {new Date ( connectionState (). connectedAt ). toLocaleTimeString () }
</ span >
</ Show >
< Show when = { connectionState (). status === ConnectionStatus . Error } >
< span > Error: { connectionState (). error ?. message } </ span >
</ Show >
</ div >
);
}
Common Patterns
Loading States
function UserList () {
const [ users , details ] = useQuery (() => zql . user );
return (
< div >
< Show when = { details (). type === 'unknown' } >
< LoadingSpinner />
</ Show >
< ul >
< For each = { users () } >
{ user => < li > { user . name } </ li > }
</ For >
</ ul >
</ div >
);
}
Error Handling
function UserList () {
const [ users , details ] = useQuery (() => zql . user );
return (
< Switch >
< Match when = { details (). type === 'error' } >
< div >
< p > Error: { details (). error ?. message } </ p >
< button onClick = { () => details (). retry ?.() } >
Retry
</ button >
</ div >
</ Match >
< Match when = { details (). type === 'complete' } >
< ul >
< For each = { users () } >
{ user => < li > { user . name } </ li > }
</ For >
</ ul >
</ Match >
</ Switch >
);
}
Mutations
function CreateUserForm () {
const zero = useZero ();
const [ name , setName ] = createSignal ( '' );
const [ isSubmitting , setIsSubmitting ] = createSignal ( false );
const handleSubmit = async ( e : Event ) => {
e . preventDefault ();
setIsSubmitting ( true );
try {
const result = zero (). mutate . user . insert ({
id: crypto . randomUUID (),
name: name (),
created: Date . now (),
});
// Wait for server confirmation
await result . server ;
setName ( '' );
} catch ( error ) {
console . error ( 'Failed to create user:' , error );
} finally {
setIsSubmitting ( false );
}
};
return (
< form onSubmit = { handleSubmit } >
< input
value = { name () }
onInput = { e => setName ( e . currentTarget . value ) }
disabled = { isSubmitting () }
/>
< button type = "submit" disabled = { isSubmitting () } >
{ isSubmitting () ? 'Creating...' : 'Create User' }
</ button >
</ form >
);
}
Derived Data
function UserStats () {
const [ users ] = useQuery (() => zql . user );
// Derived signals
const totalUsers = createMemo (() => users (). length );
const activeUsers = createMemo (() =>
users (). filter ( u => u . status === 'active' ). length
);
return (
< div >
< p > Total users: { totalUsers () } </ p >
< p > Active users: { activeUsers () } </ p >
</ div >
);
}
Multiple Queries
function Dashboard () {
const [ users ] = useQuery (() => zql . user );
const [ posts ] = useQuery (() => zql . post );
const [ comments ] = useQuery (() => zql . comment );
return (
< div >
< p > Users: { users (). length } </ p >
< p > Posts: { posts (). length } </ p >
< p > Comments: { comments (). length } </ p >
</ div >
);
}
Custom Mutations
function PublishPostButton ( props : { postID : string }) {
const zero = useZero ();
const [ isPublishing , setIsPublishing ] = createSignal ( false );
const handlePublish = async () => {
setIsPublishing ( true );
try {
const result = zero (). mutate . post . publish ({ postID: props . postID });
await result . server ;
} catch ( error ) {
console . error ( 'Failed to publish:' , error );
} finally {
setIsPublishing ( false );
}
};
return (
< button onClick = { handlePublish } disabled = { isPublishing () } >
{ isPublishing () ? 'Publishing...' : 'Publish' }
</ button >
);
}
Search and Filtering
function SearchableUserList () {
const [ searchTerm , setSearchTerm ] = createSignal ( '' );
const [ users ] = useQuery (() => {
const term = searchTerm ();
if ( ! term ) return zql . user ;
return zql . user . where ( 'name' , 'ILIKE' , `% ${ term } %` );
});
return (
< div >
< input
type = "text"
placeholder = "Search users..."
value = { searchTerm () }
onInput = { e => setSearchTerm ( e . currentTarget . value ) }
/>
< ul >
< For each = { users () } >
{ user => < li > { user . name } </ li > }
</ For >
</ ul >
</ div >
);
}
createQuery (Deprecated)
createQuery is deprecated in favor of useQuery. The API is identical.
// Old (deprecated)
const [ users ] = createQuery (() => zql . user );
// New (recommended)
const [ users ] = useQuery (() => zql . user );
TypeScript
Type-Safe Hooks
Define default types for automatic inference:
// types.ts
import { schema } from './schema' ;
import type { Context } from './auth' ;
declare module '@rocicorp/zero' {
interface DefaultTypes {
schema : typeof schema ;
context : Context ;
}
}
// Now hooks are fully typed automatically
function UserList () {
const [ users ] = useQuery (() => zql . user );
// users() is typed as User[]
const zero = useZero ();
// zero().mutate is typed with your mutators
}
Best Practices
Use Accessors : Always call users(), details(), etc. to get current values.
Create Query Builder Once : Define zql at module level, not inside components.
Leverage Reactivity : Let Solid’s reactivity track dependencies automatically in query functions.
Don’t Destructure : Don’t destructure accessors - always use users() instead of extracting values.
Comparison with React
function UserList () {
const [ users , details ] = useQuery (() => zql . user );
return (
< For each = { users () } >
{ user => < li > { user . name } </ li > }
</ For >
);
}
function UserList () {
const [ users , details ] = useQuery ( zql . user );
return (
<>
{ users . map ( user => (
< li key = { user . id } > { user . name } </ li >
)) }
</>
);
}
Key differences:
Solid uses accessors: users() instead of users
Solid query function is wrapped in an accessor: () => zql.user
Solid uses <For> for efficient list rendering
Next Steps
React Integration Compare with React integration
Queries Learn more about the query API