zero-vue

repository·main·Indexed 41 days ago

https://github.com/danielroe/zero-vue

Vue and Nuxt bindings for Zero, a general-purpose reactive data synchronization engine. It provides composables like useZero and useQuery to integrate local-first capabilities into Vue applications, including a dedicated Nuxt module for automated SSR handling and useZeroSsrQuery for hydration.

Tokens
6K
Snippets
19
Records
25
Agent score
50%

What's inside zero-vue

  1. Handle SSR manually with zero-vue

    main

    If you are not using the Nuxt module, you must manually manage the Zero instance lifecycle to avoid leaking state or connections across requests.

    1. Scope composables per request Do not call createZeroComposables at the module level in an SSR environment. Instead, instantiate it within the request lifecycle (e.g., inside a Nuxt plugin or via useNuxtApp) and ensure the instance is closed after the response is rendered.

    2. Use useAsyncData for SSR-rendered data useQuery is reactive and relies on client-side synchronization, meaning it will return an empty array during the initial synchronous SSR render. To include data in the SSR payload, use zero.run(query, { type: 'complete' }) inside an useAsyncData call and then sync the result to the live view on the client.

    3. WebSocket considerations Zero attempts to connect via WebSocket if cacheURL is provided. In Node environments where you do not want the server to connect to zero-cache, leave cacheURL unset. If you do set it, ensure you call zero.close() after rendering.

    // Example of manual SSR query implementation
    export function useZeroSsrQuery(key, query) {
      const zero = useZero()
      const { data: liveRows, status } = useQuery(query)
    
      const asyncData = useAsyncData(`zero:${key}`, async () => {
        if (!zero.value.server) {
          return []
        }
        return await zero.value.run(query, { type: 'complete' })
      }, {
        default: () => [],
        transform: rows => JSON.parse(JSON.stringify(rows)),
      })
    
      if (import.meta.client) {
        watch([liveRows, status], ([rows, status]) => {
          if (status === 'complete') {
            asyncData.data.value = rows
          }
        }, { immediate: true })
      }
    
      return asyncData
    }
  2. Integrate zero-vue with Nuxt

    main

    The zero-vue Nuxt module automates SSR handling, including request-scoped composables and automatic closing of the Zero instance after rendering.

    1. Add the module to your nuxt.config.ts:
    export default defineNuxtConfig({
      modules: ['zero-vue'],
    })
    1. Configure Zero by creating a zero.config.ts in your app/ directory using defineZeroOptions. This allows you to use Nuxt composables like useCookie or useRuntimeConfig to provide dynamic configuration.

    2. Auto-imported composables:

    • useZero(): The request-scoped Zero instance.
    • useQuery(query, options?): A reactive query view.
    • useConnectionState(): Readonly connection state.
    • useZeroSsrQuery(key, query): An useAsyncData-backed query that ensures data is present in the SSR payload and handed over to the live client view.
    • useZeroComposables(): The full request-scoped bundle.
    1. Configuration via Runtime Config: The module seeds runtimeConfig.public.zero with cacheURL, queryURL, and mutateURL. You can configure these via nuxt.config or environment variables:
    • NUXT_PUBLIC_ZERO_CACHE_URL
    • NUXT_PUBLIC_ZERO_QUERY_URL
    • NUXT_PUBLIC_ZERO_MUTATE_URL

    To enable server-side pre-fetching via WebSocket, set zero: { ssr: true } in your nuxt.config.

    // nuxt.config.ts
    export default defineNuxtConfig({
      modules: ['zero-vue'],
    })
    
    // app/zero.config.ts
    import { defineZeroOptions } from 'zero-vue'
    import { useCookie, useRuntimeConfig } from '#imports'
    import { mutators, schema } from './db/schema'
    
    export default defineZeroOptions(() => {
      const config = useRuntimeConfig()
      return {
        userID: useCookie('userID').value ?? undefined,
        cacheURL: config.public.zero.cacheURL || undefined,
        schema,
        mutators,
        kvStore: 'mem',
      }
    })
  3. Query data using useQuery and useZero

    main

    To perform queries, first access the Zero instance via useZero(), then pass a query function to useQuery(). The useQuery composable returns a reactive object containing the query results in its data property.

    import { useQuery, useZero } from './use-zero.ts'
    
    const zero = useZero()
    const { data: users } = useQuery(() => zero.value.query.user)
  4. Create Zero composables with createZeroComposables

    main

    Use createZeroComposables to generate useZero and useQuery composables. You can provide configuration as a static object, a computed getter function (useful for reactive options like userID), or by passing an existing Zero instance.

    Common options include:

    • userID: The user identifier.
    • server: The Zero server URL.
    • schema: The Zero schema.
    • mutators: The Zero mutators.
    • kvStore: The local storage engine (e.g., 'mem').
    • zero: An existing Zero instance.
    import { createZeroComposables } from 'zero-vue'
    import { mutators } from './mutators.ts'
    import { schema } from './schema.ts'
    
    // Using a static object
    const { useZero, useQuery } = createZeroComposables({
      userID,
      server: import.meta.env.VITE_PUBLIC_SERVER,
      schema,
      mutators,
      kvStore: 'mem',
    })
    
    // OR with computed options (reactive)
    const { useZero, useQuery } = createZeroComposables(() => ({
      userID: userID.value,
      server: import.meta.env.VITE_PUBLIC_SERVER,
      schema,
      mutators,
      kvStore: 'mem',
    }))
    
    // OR with an existing Zero instance
    const { useZero, useQuery } = createZeroComposables({
      zero: new Zero({
        userID,
        server: import.meta.env.VITE_PUBLIC_SERVER,
        schema,
        mutators,
        kvStore: 'mem',
      }),
    })
  5. Configure useQuery options

    main

    When calling useQuery, you can provide an optional options object to control the query behavior. Currently, this supports setting the ttl (Time To Live) for the materialized view.

    Options

    • ttl: A TTL value specifying how long the materialized view should remain valid before being considered stale.
    const options = {
      ttl: 10000 // 10 seconds
    };
    
    const { data } = useQuery(z, query, options);
  6. Configure the zero-vue Nuxt module

    main

    The zero-vue Nuxt module allows you to integrate Zero into your Nuxt application. You can configure it via the zero key in your nuxt.config.ts.

    Configuration Options

    OptionTypeDefaultDescription
    configPathstring'zero.config'Path to the file whose default export is the result of defineZeroOptions. Resolved relative to your srcDir.
    ssrbooleanfalseWhether the server-side Zero instance may connect to zero-cache during SSR. When false, cacheURL or server in your zero options are stripped on the server to prevent opening a websocket per request.
    cacheURLstring''Default for runtimeConfig.public.zero.cacheURL. Can be overridden via NUXT_PUBLIC_ZERO_CACHE_URL.
    queryURLstring''Default for runtimeConfig.public.zero.queryURL. Can be overridden via NUXT_PUBLIC_ZERO_QUERY_URL.
    mutateURLstring''Default for runtimeConfig.public.zero.mutateURL. Can be overridden via NUXT_PUBLIC_ZERO_MUTATE_URL.
    export default defineNuxtConfig({
      modules: ['zero-vue'],
      zero: {
        configPath: 'my-custom-zero.config.ts',
        ssr: true,
        cacheURL: 'https://my-cache.example.com',
        queryURL: 'https://my-query.example.com',
        mutateURL: 'https://my-mutate.example.com',
      }
    })
  7. Create composables with createZeroComposables

    main

    Use createZeroComposables to generate a set of Vue composables tailored to your specific data fetching needs. This function returns an object conforming to the ZeroComposables type, which contains the specialized hooks for your application.

    import { createZeroComposables } from 'zero-vue';
    
    const { useMyData } = createZeroComposables(options);
  8. Handle errors and retries in useQuery

    main

    When a query fails, the error property returned by useQuery will contain a QueryError object. This object includes a retry method that can be called to manually trigger a re-execution of the query.

    Error Object Structure

    • retry: A function () => void that increments an internal refetch key to force the view to update.
    • Other properties from the underlying QueryError type.
    const { error } = useQuery(z, query);
    
    // In your template or logic:
    if (error.value) {
      console.error('Query failed:', error.value.message);
      // Trigger a retry
      error.value.retry();
    }
  9. Use Zero composables in Nuxt

    main

    The zero-vue Nuxt module provides several composables to interact with the Zero instance and its data. These composables are automatically initialized and cached within the Nuxt application context.

    • useZero(): Returns the current Zero instance as a reactive value.
    • useQuery(query, options): Executes a Zero query and returns reactive results. This is the primary way to fetch and observe data.
    • useConnectionState(): Returns the current connection state of the Zero instance.
    • useZeroComposables(): An internal utility that returns the full set of generated composables.
    import { useZero, useQuery, useConnectionState } from '#imports'
    
    // Access the Zero instance
    const zero = useZero()
    
    // Execute a query
    const { data, status } = useQuery(myQuery, { /* options */ })
    
    // Monitor connection state
    const connectionState = useConnectionState()
  10. Use the useQuery composable

    main

    The useQuery composable is used to fetch and reactively track data from a Zero instance. It accepts a Zero instance, a query request, and optional configuration.

    Depending on whether the query argument is falsy, useQuery returns either a QueryResult (where data is always present) or a MaybeQueryResult (where data and status can be undefined or 'disabled').

    Parameters

    • z: A MaybeRefOrGetter of the Zero instance.
    • query: A MaybeRefOrGetter of a QueryOrQueryRequest. If this is falsy, the query is considered disabled.
    • options: A MaybeRefOrGetter of UseQueryOptions containing an optional ttl (Time To Live).

    Returns

    A reactive object containing:

    • data: A ComputedRef of the query results (formatted as HumanReadable<TReturn>).
    • status: A ComputedRef of the current QueryStatus (e.g., 'loading', 'error', 'success', or 'disabled').
    • error: A ComputedRef containing the QueryError and a retry() function to manually trigger a refetch, or undefined if there is no error.
    import { useQuery } from 'zero-vue';
    
    // Assuming 'z' is your Zero instance
    const { data, status, error } = useQuery(
      z,
      () => z.tables.users.query({ where: { id: '123' } }),
      { ttl: 5000 }
    );
    
    // Accessing reactive data
    // data.value will contain the human-readable results
  11. Handle SSR with useZeroSsrQuery

    main

    When using Nuxt with SSR, standard reactive queries might not have data ready during the server-side render. useZeroSsrQuery bridges Zero's reactive useQuery with Nuxt's useAsyncData to ensure the first render contains data.

    How it works:

    1. Server-side: It attempts to run the query using zero.run. If the Zero instance has no server configured or if the query takes longer than 10 seconds (SSR_RUN_TIMEOUT_MS), it falls back to an empty array to prevent hanging the server response.
    2. Client-side: Once the client hydrates, it watches the live useQuery results. When the Zero query status becomes 'complete', it updates the asyncData value to sync the live reactive view with the Zero cache.

    Note: The function automatically transforms the data via JSON.parse(JSON.stringify(rows)) to strip internal symbol-keyed refcount properties that cannot be serialized into the Nuxt payload.

    import { useZeroSsrQuery } from '#imports'
    
    // Use this in Nuxt components to ensure data is available during SSR
    const { data } = useZeroSsrQuery('my-unique-key', myQuery)