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:
| State | What's happening |
|---|---|
uninitialized | client.init() hasn't been called |
platformClassesInit | Loading the platform-specific (browser or Node) classes |
fileSystemInit | Mounting the browser's private file system (OPFS) |
dbInit | Opening the SQLite database |
saveConfig | Saving the app's config to the database |
processSchemaFiles | Loading your schema (config.schema) |
addModelsToDb | Writing models to the database |
idle | Ready. Safe to read and write |
initFailed | A 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
| Field | Type | Description |
|---|---|---|
status | ClientManagerState | 'stopped' | The current state from the table above, or 'stopped' after client.stop(). 'idle' means the client is ready |
internalStatus | ClientManagerState | 'ready' | 'stopped' | 'ready' while the database is open: from saveConfig through idle. Otherwise the same value as status (including dbInit, initFailed and stopped) |
stopReason | ClientStopReason | undefined | Why 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(): booleanReturns
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): booleanBoth 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): TuseClientService() 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
}| Field | Type | Description |
|---|---|---|
services | ActorRef[] | An array with one entry, the current client manager's XState actor (uniqueKey set to 'clientManager') |
percentComplete | number | Follows 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
}| Field | Description |
|---|---|
name | getServiceName(service) |
timeElapsed | Seconds since mount, counted once a second until the actor reaches done, success, idle or ready |
value | getServiceValue(service) |
percentComplete | For 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 |
uniqueKey | getServiceUniqueKey(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| Function | Returns |
|---|---|
getServiceName | The actor's uniqueKey property if set, otherwise getServiceUniqueKey(service), otherwise 'actor' |
getServiceValue | The actor's state value. Object values are JSON-stringified. An actor named global in an initialized state reports 'ready' |
getServiceUniqueKey | A 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(): booleanDeprecated no-ops, to be removed: usePersistedSnapshots does nothing and useHasSavedSnapshots always returns false. Nothing writes snapshots. Don't build on either.
Related
- Overview and setup:
SeedProviderandSeedClientGate - Client and configuration:
client.init()and its options - Multi-tab behavior: why init can fail with
FileSystemLockedError