Zero Svelte

repository·main·Indexed 42 days ago

https://github.com/stolinski/zero-svelte

A local-first platform integration for Svelte version 2.0.0 that enables reactive applications using Zero's sync engine. It provides the Z class for state management, a Query class for reactive data fetching via Svelte 5 runes, and tools for managing connection lifecycles, authentication, and PostgreSQL backend configuration via Docker Compose.

Tokens
8.6K
Snippets
28
Records
35
Agent score
54%

What's inside Zero Svelte

  1. Monitor and Manage Connection State

    main

    You can monitor the connection status via z.connectionState. Possible states include: connecting, connected, disconnected, needs-auth, error, and closed.

    If the connection enters an error or auth-required state, you can manually retry using z.connection.connect(), optionally passing a new auth token.

    <script lang="ts">
    	import { z } from '$lib/zero.svelte';
    </script>
    
    {#if z.connectionState.name === 'connected'}
    	<span>Connected</span>
    {:else if z.connectionState.name === 'connecting'}
    	<span>Connecting...</span>
    {:else if z.connectionState.name === 'disconnected'}
    	<span>Disconnected: {z.connectionState.reason}</span>
    {:else if z.connectionState.name === 'needs-auth'}
    	<span>
    		Auth Required
    		<button onclick={() => z.connection.connect()}>Retry</button>
    	</span>
    {:else if z.connectionState.name === 'error'}
    	<span>
    		Error: {z.connectionState.reason}
    		<button onclick={() => z.connection.connect()}>Retry</button>
    	</span>
    {:else if z.connectionState.name === 'closed'}
    	<span>Closed</span>
    {/if}
    // Retry connection
    await z.connection.connect();
    
    // Retry with new auth token
    await z.connection.connect({ auth: newToken });
  2. Disable SSR for Zero Svelte

    main

    Zero Svelte requires client-side execution. Ensure Server-Side Rendering (SSR) is disabled in your SvelteKit configuration (e.g., in +page.ts or +layout.ts).

    export const ssr = false;
  3. Initialize the Z instance

    main

    Create a single instance of Z to manage your local-first state. Because of the DefaultTypes declaration in your schema file, you do not need to provide explicit generics here.

    import { PUBLIC_SERVER } from '$env/static/public';
    import { Z } from 'zero-svelte';
    import { mutators } from '../mutators.js';
    import { schema } from '../schema.js';
    
    export const z = new Z({
    	cacheURL: PUBLIC_SERVER,
    	schema,
    	mutators,
    	userID: 'anon',
    	kvStore: 'mem'
    });
    
    export { mutators };
  4. Configure Schema and Type-Safe Queries

    main

    Define your database schema and reusable, type-safe queries using @rocicorp/zero. You can use defineQueries to create validated, reusable query objects. To avoid passing the schema as a generic to every Zero instance, register your schema as the DefaultTypes in the @rocicorp/zero module.

    import {
    	boolean,
    	createBuilder,
    	createSchema,
    	defineQueries,
    	defineQuery,
    	relationships,
    	string,
    	table
    } from '@rocicorp/zero';
    import { type } from 'arktype'; // or use zod
    
    const types = table('type').columns({ id: string(), name: string() }).primaryKey('id');
    
    const todos = table('todo')
    	.columns({
    		id: string(),
    		title: string(),
    		completed: boolean(),
    		type_id: string()
    	})
    	.primaryKey('id');
    
    const todoRelationship = relationships(todos, ({ one }) => ({
    	type: one({
    		sourceField: ['type_id'],
    		destField: ['id'],
    		destSchema: types
    	})
    }));
    
    export const schema = createSchema({
    	tables: [types, todos],
    	relationships: [todoRelationship]
    });
    
    export type Schema = typeof schema;
    
    export const zql = createBuilder(schema);
    
    export const queries = defineQueries({
    	todo: {
    		all: defineQuery(() => zql.todo.related('type')),
    		byCompleted: defineQuery(type({ completed: 'boolean' }), ({ args: { completed } }) =>
    			zql.todo.where('completed', '=', completed).related('type')
    	)
    	},
    	type: {
    		all: defineQuery(() => zql.type)
    	}
    });
    
    declare module '@rocicorp/zero' {
    	interface DefaultTypes {
    		schema: Schema;
    	}
    }
  5. How ViewStore and ViewWrapper work together

    main

    The ViewStore and ViewWrapper classes manage the lifecycle of materialized Zero views within Svelte.

    • ViewStore: Acts as a cache for ViewWrapper instances. It uses a hash of the query to ensure that if multiple components request the same query, they share the same underlying ViewWrapper and subscription, preventing redundant work.
    • ViewWrapper: Represents a single materialized view. It manages a Svelte subscription to the underlying Zero TypedView. When the view is active, it provides reactive access to the data.

    Accessing Data:

    • wrapper.current: A reactive getter that returns a tuple [data, details]. Accessing this property triggers Svelte's subscription tracking, making the component re-render when the data changes.
    • wrapper.dataOnly: A non-reactive getter that returns the data without triggering Svelte updates (useful for logic inside $derived or non-UI code).
    • wrapper.detailsOnly: Returns the QueryResultDetails (e.g., the status of the query).
  6. Run the Zero Svelte PostgreSQL backend with Docker Compose

    main

    To run a local PostgreSQL instance configured for Zero Svelte (supporting logical replication), use the provided docker-compose.yml. This configuration sets up a Postgres 16.2 instance with the necessary WAL settings for logical replication and exposes it on port 5430.

    services:
      zstart_postgres:
        image: postgres:16.2-alpine
        shm_size: 1g
        user: postgres
        restart: always
        healthcheck:
          test: 'pg_isready -U user --dbname=postgres'
          interval: 10s
          timeout: 5s
          retries: 5
        ports:
          - 5430:5432
        environment:
          POSTGRES_USER: user
          POSTGRES_DB: postgres
          POSTGRES_PASSWORD: password
        command: |
          postgres 
          -c wal_level=logical
          -c max_wal_senders=10 
          -c max_replication_slots=5 
          -c hot_standby=on 
          -c hot_standby_feedback=on
        volumes:
          - zstart_pgdata:/var/lib/postgresql/data
          - ./:/docker-entrypoint-initdb.d
    
    volumes:
      zstart_pgdata:
        driver: local
  7. Initialize the Zero instance

    main

    To use Zero in your Svelte application, you must instantiate the Z class with a configuration object. This instance (typically exported as z) serves as the central entry point for interacting with your data, managing schema, and executing mutations.

    Required configuration keys:

    • cacheURL: The URL of your Zero server (e.g., from an environment variable like PUBLIC_SERVER).
    • schema: The schema object defining your data structure.
    • mutators: The collection of mutation functions used to modify data.

    Optional configuration keys:

    • userID: A unique identifier for the user (defaults to 'anon').
    • kvStore: The key-value store implementation (e.g., 'mem' for in-memory).
    import { Z } from '$lib/Z.svelte.js';
    import { PUBLIC_SERVER } from '$env/static/public';
    import { schema } from '../schema.js';
    import { mutators } from '../mutators.js';
    
    export const z = new Z({
    	cacheURL: PUBLIC_SERVER,
    	schema,
    	mutators,
    	userID: 'anon',
    	kvStore: 'mem'
    });
  8. Create Reactive Queries with $derived

    main

    To make queries react to local Svelte state changes, wrap z.createQuery in a Svelte $derived rune. When the state variable changes, the query will automatically re-run.

    <script lang="ts">
    	import { z } from '$lib/zero.svelte';
    	import { queries } from '../schema.js';
    
    	let showCompleted = $state(false);
    
    	// Query recreates when showCompleted changes
    	const filtered = $derived(z.createQuery(queries.todo.byCompleted({ completed: showCompleted })));
    </script>
    
    <label>
    	<input type="checkbox" bind:checked={showCompleted} />
    	Show completed only
    </label>
    
    {#each filtered.data as todo (todo.id)}
    	<div>{todo.title}</div>
    {/each}
  9. Perform Local-only Filtering with Raw ZQL

    main

    For high-performance client-side filtering (like search-as-you-type) that doesn't require server round-trips, use the zql builder inside a $derived.by rune. This filters data that has already been synced to the local cache.

    <script lang="ts">
    	import { z } from '$lib/zero.svelte';
    	import { zql } from '../schema.js';
    
    	let searchText = $state('');
    
    	// Filters data already synced - no server round-trip per keystroke
    	const searchResults = $derived.by(() => {
    		let q = zql.todo.related('type');
    		if (searchText) {
    			q = q.where('title', 'ILIKE', `%${searchText}%`);
    		}
    		return z.createQuery(q);
    	});
    </script>
    
    <input type="text" placeholder="Search..." bind:value={searchText} />
    
    {#each searchResults.data as todo (todo.id)}
    	<div>{todo.title}</div>
    {/each}
  10. Update Existing Queries with updateQuery

    main

    Instead of creating new query instances, use query.updateQuery(newQuery) to update an existing query in place. This is useful for maintaining stable query references while changing filters.

    <script lang="ts">
    	import { z } from '$lib/zero.svelte';
    	import { zql } from '../schema.js';
    
    	const todos = z.createQuery(zql.todo.related('type'));
    
    	function applyFilter(typeId: string | undefined) {
    		const q = typeId
    			? zql.todo.where('type_id', '=', typeId).related('type')
    			: zql.todo.related('type');
    		todos.updateQuery(q);
    	}
    </script>
  11. Gate Queries with the Enabled Flag

    main

    You can prevent a query from materializing until a certain condition is met by passing an enabled boolean as the second argument to z.createQuery().

    <script lang="ts">
    	import { z } from '$lib/zero.svelte';
    	import { queries } from '../schema.js';
    
    	let enabled = $state(true);
    
    	// Query won't materialize until enabled is true
    	const todos = $derived(z.createQuery(queries.todo.all(), enabled));
    </script>
    
    <label>
    	<input type="checkbox" bind:checked={enabled} />
    	Enable query
    </label>