Skip to main content

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>
  );
}
Key differences:
  1. Solid uses accessors: users() instead of users
  2. Solid query function is wrapped in an accessor: () => zql.user
  3. Solid uses <For> for efficient list rendering

Next Steps

React Integration

Compare with React integration

Queries

Learn more about the query API

Build docs developers (and LLMs) love