React
Zero has built-in support for React. Here's what basic usage looks like.
Setup
Use the ZeroProvider component to setup Zero. It takes care of creating and destroying Zero instances reactively:
import {createRoot} from 'react-dom/client'
import {ZeroProvider} from '@rocicorp/zero/react'
import {useSession} from 'my-session-provider'
import App from './App.tsx'
import {schema} from 'schema.ts'
import {mutators} from 'mutators.ts'
const cacheURL = import.meta.env.VITE_PUBLIC_ZERO_CACHE_URL!
export default function Root() {
const session = useSession()
const userID = session?.userID
const auth = session?.accessToken
const context = userID ? {userID} : undefined
return (
<ZeroProvider
{...{
userID,
auth,
context,
cacheURL,
schema,
mutators
}}
>
<App />
</ZeroProvider>
)
}If you use token auth, pass it with auth={session?.jwt}. If you use cookie auth, omit the parameter entirely.
When auth changes from one string to another, ZeroProvider refreshes auth in place with zero.connection.connect({auth}). When auth is added or removed, or userID changes, it recreates the Zero instance.
When the user is logged out, omit the userID entirely.
You can also pass a Zero instance to the ZeroProvider if you want to control the lifecycle of the Zero instance yourself:
// ZeroProvider just sets up the context, it doesn't manage
// the lifecycle of the Zero instance.
<ZeroProvider zero={zero}>
<App />
</ZeroProvider>When you pass zero={zero}, ZeroProvider only provides React Context. It does not manage auth updates for that instance, so if you are using token auth, call zero.connection.connect({auth: newToken}) yourself or recreate the instance when the user changes.
Usage
Use useQuery to run queries:
import {useQuery} from '@rocicorp/zero/react'
import {queries} from 'queries.ts'
function Posts() {
const [posts] = useQuery(
queries.posts.byStatus({status: 'draft'})
)
return (
<>
{posts.map(p => (
<div key={p.id}>
{p.title} ({p.comments.length} comments)
</div>
))}
</>
)
}For conditional queries, such as queries that depend on auth state or route params that may not be loaded yet, see Conditional Queries.
Use useZero to get access to the Zero instance, for example to run mutators:
import {useZero} from '@rocicorp/zero/react'
import {mutators} from 'mutators.ts'
function CompleteButton({issueID}: {issueID: string}) {
const zero = useZero()
const onClick = () => {
zero.mutate(mutators.issues.complete({id: issueID}))
}
return <button onClick={onClick}>Complete Issue</button>
}Suspense
The useSuspenseQuery hook is exactly like useQuery, except it supports React Suspense.
const [issues] = useSuspenseQuery(issueQuery, {
suspendUntil: 'complete' // 'partial' or 'complete'
})Use the suspendUntil parameter to control how long to suspend for. The value complete suspends until authoritative results from the server are received. The partial value suspends until any non-empty data is received, or for a empty result that is complete.
Query Identity
You don't need to memoize queries. Build them inline, where you use them, and pass them straight to useQuery.
Zero caches the queries you build with ZQL. Build the same query again and you usually get back the same Query object, even when it has related() or whereExists() callbacks written inline. So rebuilding a query on every render is cheap. You can also use a query as a dependency of useEffect or useMemo, or pass it as a prop to a component wrapped in React.memo:
import {useEffect} from 'react'
import {useQuery} from '@rocicorp/zero/react'
import {zql} from 'schema.ts'
function IssueSearch({text}: {text: string}) {
// Rebuilt on every render. No useMemo needed.
const query = zql.issue
.where('title', 'ILIKE', `%${text}%`)
.orderBy('created', 'desc')
const [issues] = useQuery(query)
// Scroll back to the top when the search changes.
useEffect(() => {
window.scrollTo(0, 0)
}, [query])
return issues.map(issue => (
<div key={issue.id}>{issue.title}</div>
))
}This example passes ZQL to useQuery directly, so it runs only on the client.
Calls to registered queries work a little differently. Each call like queries.posts.byStatus({status}) returns a new object. You can still pass it inline: useQuery sees that it's the same query with the same arguments and reuses its previous work. But don't use the returned object as a dependency. Use its arguments instead.
Identity follows the order you build a query in. zql.issue.where('status', 'open').where('priority', 'high') and zql.issue.where('priority', 'high').where('status', 'open') return the same rows but can be different objects. A different value, such as another ID in where(), always gives a different object.
Zero doesn't guarantee identity, though. It can hand back a new object for a query it has built before. When that happens, an effect or memo that depends on the query runs again, so make sure that's safe.
Examples
See the sample directory for more complete React examples.