Overview
Zero provides React hooks for seamless integration with React applications. The hooks handle subscription management, automatic updates, and proper cleanup.
Installation
npm install @rocicorp/zero
Setup
ZeroProvider
Wrap your app with ZeroProvider to make Zero available to all components:
import { ZeroProvider } from '@rocicorp/zero/react' ;
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 >
Dynamic Authentication
Update auth token without recreating the Zero client:
function App () {
const [ authToken , setAuthToken ] = useState < string | undefined >();
return (
< ZeroProvider
schema = { schema }
server = "https://your-zero-server.com"
userID = "user-123"
auth = { authToken } // Provider handles auth changes
>
< LoginHandler onToken = { setAuthToken } />
< YourApp />
</ ZeroProvider >
);
}
External Zero Instance
Pass an existing Zero instance instead of creating one:
import { Zero } from '@rocicorp/zero' ;
const zero = new Zero ({
schema ,
server: 'https://your-zero-server.com' ,
userID: 'user-123' ,
});
function App () {
return (
< ZeroProvider zero = { zero } >
< YourApp />
</ ZeroProvider >
);
}
useQuery
The useQuery hook subscribes to a query and returns reactive data:
import { useQuery } from '@rocicorp/zero/react' ;
import { createBuilder } from '@rocicorp/zero' ;
import { schema } from './schema' ;
const zql = createBuilder ( schema );
function UserList () {
const [ users , resultDetails ] = useQuery ( zql . user );
if ( resultDetails . type === 'unknown' ) {
return < div > Loading... </ div > ;
}
if ( resultDetails . type === 'error' ) {
return < div > Error: { resultDetails . error . message } </ div > ;
}
return (
< ul >
{ users . map ( user => (
< li key = { user . id } > { user . name } </ li >
)) }
</ ul >
);
}
Query Result
useQuery returns a tuple:
const [ data , details ] = useQuery ( query );
data: The query result (array or single value)
details: Query metadata with type property:
unknown - Loading, data may be partial
complete - Fully loaded from server
error - Query failed
Conditional Queries
Pass falsy values to disable queries:
function UserProfile ({ userID } : { userID : string | null }) {
// Query is disabled when userID is null
const [ user , details ] = useQuery (
userID ? zql . user . where ( 'id' , userID ). one () : null
);
if ( ! userID ) {
return < div > Select a user </ div > ;
}
if ( details . type === 'unknown' ) {
return < div > Loading... </ div > ;
}
return < div > { user ?. name ?? 'Not found' } </ div > ;
}
Query Options
// Disable query based on condition
const [ users ] = useQuery ( query , { enabled: isLoggedIn });
// Or use boolean shorthand
const [ users ] = useQuery ( query , isLoggedIn );
// Set TTL (time to live)
const [ users ] = useQuery ( query , {
enabled: true ,
ttl: '5m' , // Keep alive for 5 minutes
});
useSuspenseQuery
For React Suspense integration:
import { Suspense } from 'react' ;
import { useSuspenseQuery } from '@rocicorp/zero/react' ;
function UserList () {
// Suspends until data is ready
const [ users ] = useSuspenseQuery ( zql . user );
return (
< ul >
{ users . map ( user => (
< li key = { user . id } > { user . name } </ li >
)) }
</ ul >
);
}
function App () {
return (
< Suspense fallback = { < div > Loading... </ div > } >
< UserList />
</ Suspense >
);
}
Suspend Until Options
// Suspend until partial data (default)
const [ users ] = useSuspenseQuery ( query , {
suspendUntil: 'partial'
});
// Suspend until complete data from server
const [ users ] = useSuspenseQuery ( query , {
suspendUntil: 'complete'
});
useZero
Access the Zero instance directly:
import { useZero } from '@rocicorp/zero/react' ;
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 > ;
}
useConnectionState
Monitor the connection status:
import { useConnectionState } from '@rocicorp/zero/react' ;
import { ConnectionStatus } from '@rocicorp/zero' ;
function ConnectionIndicator () {
const connectionState = useConnectionState ();
return (
< div >
Status: { connectionState . status }
{ connectionState . status === ConnectionStatus . Connected && (
< span > Connected at {new Date ( connectionState . connectedAt ). toLocaleTimeString () } </ span >
) }
{ connectionState . status === ConnectionStatus . Error && (
< span > Error: { connectionState . error ?. message } </ span >
) }
</ div >
);
}
Common Patterns
Loading States
function UserList () {
const [ users , { type }] = useQuery ( zql . user );
return (
< div >
{ type === 'unknown' && < LoadingSpinner /> }
< ul >
{ users . map ( user => (
< li key = { user . id } > { user . name } </ li >
)) }
</ ul >
</ div >
);
}
Error Handling
function UserList () {
const [ users , details ] = useQuery ( zql . user );
if ( details . type === 'error' ) {
return (
< div >
< p > Error: { details . error . message } </ p >
< button onClick = { details . retry } > Retry </ button >
</ div >
);
}
return (
< ul >
{ users . map ( user => (
< li key = { user . id } > { user . name } </ li >
)) }
</ ul >
);
}
Dynamic Queries
function FilteredUserList () {
const [ status , setStatus ] = useState < 'active' | 'inactive' >( 'active' );
const [ users ] = useQuery ( zql . user . where ( 'status' , status ));
return (
< div >
< select value = { status } onChange = { e => setStatus ( e . target . value ) } >
< option value = "active" > Active </ option >
< option value = "inactive" > Inactive </ option >
</ select >
< ul >
{ users . map ( user => (
< li key = { user . id } > { user . name } </ li >
)) }
</ ul >
</ div >
);
}
Mutations
function CreateUserForm () {
const zero = useZero ();
const [ name , setName ] = useState ( '' );
const [ isSubmitting , setIsSubmitting ] = useState ( false );
const handleSubmit = async ( e : React . FormEvent ) => {
e . preventDefault ();
setIsSubmitting ( true );
try {
const result = zero . mutate . user . insert ({
id: crypto . randomUUID (),
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 }
onChange = { e => setName ( e . target . value ) }
disabled = { isSubmitting }
/>
< button type = "submit" disabled = { isSubmitting } >
{ isSubmitting ? 'Creating...' : 'Create User' }
</ button >
</ form >
);
}
Custom Mutations
function PublishPostButton ({ postID } : { postID : string }) {
const zero = useZero ();
const [ isPublishing , setIsPublishing ] = useState ( false );
const handlePublish = async () => {
setIsPublishing ( true );
try {
const result = zero . mutate . post . publish ({ 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 >
);
}
function PaginatedUserList () {
const [ page , setPage ] = useState ( 0 );
const PAGE_SIZE = 20 ;
const [ users , { type }] = useQuery (
zql . user
. orderBy ( 'created' , 'desc' )
. limit ( PAGE_SIZE )
// In practice, you'd implement cursor-based pagination
);
return (
< div >
{ type === 'unknown' && < div > Loading... </ div > }
< ul >
{ users . map ( user => (
< li key = { user . id } > { user . name } </ li >
)) }
</ ul >
< button onClick = { () => setPage ( p => p - 1 ) } disabled = { page === 0 } >
Previous
</ button >
< button onClick = { () => setPage ( p => p + 1 ) } >
Next
</ button >
</ div >
);
}
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
Create Query Builder Once : Define zql at module level, not inside components.
Use Suspense for Simple Cases : useSuspenseQuery simplifies loading states in many cases.
Handle Errors Gracefully : Always check for error state and provide retry functionality.
Don’t Create Zero in Render : Use ZeroProvider or useMemo to avoid recreating the Zero instance.
Next Steps
Solid Integration Use Zero with Solid.js
React Native Use Zero in React Native apps