Seed Protocol
@seedprotocol/react

Client and DB status

Hooks that report how far the Seed client has got through startup, so you can wait for it before calling the SDK directly.

These hooks read the state of the SDK's client manager, the state machine that client.init() drives from uninitialized to idle. Use them when something outside Seed's own hooks has to wait for the client, or to show startup progress.

import { useGlobalServiceStatus, useIsClientReady, useDbsAreReady, useIsDbReady } from '@seedprotocol/react'

None of these hooks need SeedProvider, and they work whether or not SeedClientGate is mounted.

Startup states

client.init() moves the client manager through these states, in order:

StateWhat's happening
uninitializedclient.init() hasn't been called
platformClassesInitLoading the platform-specific (browser or Node) classes
fileSystemInitMounting the browser's private file system (OPFS)
dbInitOpening the SQLite database
saveConfigSaving the app's config to the database
processSchemaFilesLoading your schema (config.schema)
addModelsToDbWriting models to the database
idleReady. Safe to read and write
initFailedA step failed. client.init() rejects with the cause

The ClientManagerState enum in @seedprotocol/sdk has these values. From client.stop() until the next init starts, useGlobalServiceStatus reports stopped, which isn't in the enum.

useGlobalServiceStatus

Returns the client manager's current state. status === 'idle' is the check to use before calling the SDK directly.

function useGlobalServiceStatus(): {
  status: ClientManagerState | 'stopped'
  internalStatus: ClientManagerState | 'ready' | 'stopped'
  stopReason: ClientStopReason | undefined
}

ClientManagerState and ClientStopReason ('stop' | 'reset') are exported from @seedprotocol/sdk.

Returns

FieldTypeDescription
statusClientManagerState | 'stopped'The current state from the table above, or 'stopped' after client.stop(). 'idle' means the client is ready
internalStatusClientManagerState | 'ready' | 'stopped''ready' while the database is open: from saveConfig through idle. Otherwise the same value as status (including dbInit, initFailed and stopped)
stopReasonClientStopReason | undefinedWhy the client stopped, while status is 'stopped': 'reset' when client.resetLocalData() in this tab or another tab of the same database stopped it, 'stop' for client.stop() or client.unload(). undefined while the client isn't stopped

The hook re-renders whenever the state changes. After client.stop(), both fields are 'stopped' until the next client.init() starts (see After client.stop()). 'stopped' isn't a ClientManagerState value, but it's part of status's type, so comparing status with a string outside the union is a type error.

Example

SeedClientGate keeps its children mounted while the client starts, so an effect that calls the SDK on mount must wait for idle:

import { useEffect, useState } from 'react'
import { Item } from '@seedprotocol/sdk'
import { useGlobalServiceStatus } from '@seedprotocol/react'

export function DraftCount() {
  const { status } = useGlobalServiceStatus()
  const isReady = status === 'idle'
  const [count, setCount] = useState<number | null>(null)

  useEffect(() => {
    if (!isReady) return
    Item.all('Post').then((posts) => setCount(posts.length))
  }, [isReady])

  if (status === 'initFailed') return <p>The client failed to start.</p>
  if (!isReady) return <p>Starting… ({status})</p>
  return <p>{count ?? '…'} drafts</p>
}

Seed's own hooks (useItems, useSchema and so on) already wait for idle, so you don't need this check around them.

internalStatus === 'ready' does not mean the client is ready. It turns 'ready' once the database is open, before the schema and models are loaded. Use status === 'idle'.

useIsClientReady

Returns true while the client is ready: the same check as useGlobalServiceStatus().status === 'idle', as a boolean. SeedClientGate and Seed's data hooks use it to wait for the client.

function useIsClientReady(): boolean

Returns

true while the client manager is in idle. false before client.init() is called, during every startup state, and in initFailed. A retry after initFailed runs through the startup states again, so it stays false until that retry reaches idle. Once the client is idle, calling client.init() again with the same config doesn't restart startup. After client.stop() it's false until the next init reaches idle; see After client.stop().

import { useIsClientReady } from '@seedprotocol/react'

function SaveButton({ onSave }: { onSave: () => void }) {
  const isClientReady = useIsClientReady()
  return (
    <button onClick={onSave} disabled={!isClientReady}>
      Save
    </button>
  )
}

Use useGlobalServiceStatus instead when you also need to tell "still starting" from initFailed.

@seedprotocol/react also exports the SDK's isClientReadySnapshot(snapshot), the same check for a client actor snapshot you already have, as described in isClientReady and whenDbReady.

useDbsAreReady and useIsDbReady

One signal with two return shapes: whether the app database is open. Pick either.

function useDbsAreReady(): { dbsAreReady: boolean }
function useIsDbReady(): boolean
function isDbReadyState(state: unknown): boolean

Both are true from saveConfig through idle, once dbInit has opened the database. They're false before that, during dbInit, in initFailed (and again while a retry runs), and after client.stop(). useIsDbReady() is useGlobalServiceStatus().internalStatus === 'ready'. isDbReadyState(state) is the predicate behind both, for a state you already have, such as client.getService().getSnapshot().value; it returns false for anything that isn't one of those state strings.

The signal turns true earlier than status === 'idle': before the schema and models are loaded. Use it for a progress indicator, not as a signal that reads and writes will work.

import { useDbsAreReady } from '@seedprotocol/react'

function DbBadge() {
  const { dbsAreReady } = useDbsAreReady()
  return <span>{dbsAreReady ? 'Database open' : 'Starting…'}</span>
}

After client.stop()

client.stop() stops the client manager and closes the database, and the next client.init() starts a new one. The status hooks and SeedClientGate follow whichever client manager is current, without a remount: from stop() until the next init reaches idle, useIsClientReady(), useDbsAreReady() and useIsDbReady() are false, useServices().percentComplete is 0, and the gate shows its loading state. useGlobalServiceStatus() reports 'stopped' in both fields, with stopReason 'stop' (or 'reset' after client.resetLocalData()), until the new init starts. Once it starts, the hooks report its startup states as on the first start.

Instances loaded before the stop are stopped: writes on them throw EntityUnloadedError. The hooks that showed them look them up again once the next init reaches idle.

Client calls made after stop(), such as client.getAddresses(), throw ClientNotInitializedError (code 'CLIENT_NOT_INITIALIZED') until the next init finishes, as they do before the first init. Check err instanceof ClientNotInitializedError or err.code rather than the message.

To switch configs, await client.stop() and pass the new config to the gate's initConfig. The gate calls client.init() with it, so a failure reaches onInitError:

import { useState, type ReactNode } from 'react'
import { client, type SeedConstructorOptions } from '@seedprotocol/sdk'
import { SeedClientGate } from '@seedprotocol/react'
import { config, archiveConfig } from './seed'

export function Root({ children }: { children: ReactNode }) {
  const [initConfig, setInitConfig] = useState<SeedConstructorOptions>({ config })

  const switchTo = async (next: SeedConstructorOptions) => {
    await client.stop()
    setInitConfig(next)
  }

  return (
    <>
      <button onClick={() => switchTo({ config: archiveConfig })}>Open archive</button>
      <SeedClientGate initConfig={initConfig} onInitError={(err) => console.error(err)}>
        {children}
      </SeedClientGate>
    </>
  )
}

useClientService and useClientSelector

The hooks the status hooks are built on, for reading something else from the client manager.

function useClientService(): ClientService
function useClientSelector<T>(selector: (snapshot: ClientSnapshot) => T): T

useClientService() returns the current client manager actor, the same object as client.getService(), and re-renders when client.init() after client.stop() replaces it. useClientSelector(selector) returns selector(snapshot) for the current actor's XState snapshot (value, status, context) and re-renders when the result changes, including across a stop. The selector must return a primitive or a stable reference: a new object on every call re-renders forever.

import { useClientSelector } from '@seedprotocol/react'

function ClientState() {
  const state = useClientSelector((snapshot) => `${String(snapshot.value)} (${snapshot.status})`)
  return <code>{state}</code>
}

ClientService and ClientSnapshot stand for the actor's and its snapshot's types; the package doesn't export names for them.

Low-level: services and progress

These exports date from when startup ran across several actors; now they only see the one client manager. They're public, but useGlobalServiceStatus gives the same information more directly.

useServices

Returns the client manager actor and a coarse startup percentage.

function useServices(): {
  services: ActorRef<any, any>[]
  percentComplete: number
}
FieldTypeDescription
servicesActorRef[]An array with one entry, the current client manager's XState actor (uniqueKey set to 'clientManager')
percentCompletenumberFollows the client manager's state: 10 platform classes, 30 file system, 50 database, 60 save config, 70 schema files, 90 models, 100 idle. 0 for uninitialized, initFailed and after client.stop()

The hook re-renders on each state change, and follows the new client manager when client.init() runs after client.stop().

useService

Status for one actor, for debug panels.

function useService(service: ActorRef<any, any>): {
  name: string
  timeElapsed: number
  value: string | undefined
  percentComplete: number
  uniqueKey: any
}
FieldDescription
namegetServiceName(service)
timeElapsedSeconds since mount, counted once a second until the actor reaches done, success, idle or ready
valuegetServiceValue(service)
percentCompleteFor the client manager, the same steps as useServices().percentComplete. For other actors, 100 in a final state (idle, ready, done, success, initialized), otherwise the current state's position among the states tagged loading
uniqueKeygetServiceUniqueKey(service)

The hook re-renders when the actor's state changes, and once a second while timeElapsed counts.

getServiceName, getServiceValue, getServiceUniqueKey

Plain functions that label an XState actor for display.

function getServiceName(service: ActorRef<any, any>): string
function getServiceValue(service: ActorRef<any, any>): string | undefined
function getServiceUniqueKey(service: ActorRef<any, any>): any
FunctionReturns
getServiceNameThe actor's uniqueKey property if set, otherwise getServiceUniqueKey(service), otherwise 'actor'
getServiceValueThe actor's state value. Object values are JSON-stringified. An actor named global in an initialized state reports 'ready'
getServiceUniqueKeyA key derived from the machine ID (the part after @seedSdk/), replaced by the context's dbName, modelNamePlural or pluralized modelName when present. undefined if the actor has no machine config. Typed as any

usePersistedSnapshots and useHasSavedSnapshots

function usePersistedSnapshots(): void
function useHasSavedSnapshots(): boolean

Deprecated no-ops, to be removed: usePersistedSnapshots does nothing and useHasSavedSnapshots always returns false. Nothing writes snapshots. Don't build on either.

On this page