LocalMode
React

Utilities

Hooks for model loading, model status, capabilities, network, storage, voice recording, audit logs, and helper utilities for files and downloads.

Utility Hooks

See it in action

Try the Chat block, Knowledge Base block, and Audio Studio block for working demos of these hooks.

useVoiceRecorder

Manage browser MediaRecorder lifecycle for audio recording. Pairs naturally with useTranscribe.

import { useVoiceRecorder, useTranscribe } from '@localmode/react';

function VoiceInput() {
  const recorder = useVoiceRecorder();
  const transcriber = useTranscribe({ model });

  const handleStop = async () => {
    const blob = await recorder.stopRecording();
    if (blob) await transcriber.execute(blob);
  };

  return (
    <div>
      <button onClick={recorder.isRecording ? handleStop : recorder.startRecording}>
        {recorder.isRecording ? 'Stop' : 'Record'}
      </button>
      {recorder.error && <p>{recorder.error.message}</p>}
    </div>
  );
}

Return Value

PropertyTypeDescription
isRecordingbooleanWhether audio is being recorded
streamMediaStream | nullThe live MediaStream while recording (e.g. for waveform visualizers), null otherwise
errorAppError | nullRecording error (e.g., mic denied)
startRecording() => Promise<void>Request mic access and start recording
stopRecording() => Promise<Blob | null>Stop recording, return audio blob
getVolume() => numberCurrent input volume as RMS in [0, 1] (0 when not recording)
clearError() => voidClear the error state

Options

OptionTypeDescription
mimeTypestringPreferred MIME type (default: 'audio/webm;codecs=opus')
deviceIdstringSpecific microphone to record from (a deviceId from enumerateDevices()). Forwarded as { deviceId: { exact: deviceId } } — recording fails with error set if the device is unavailable, rather than silently falling back
constraintsMediaTrackConstraintsAdditional audio track constraints merged into the getUserMedia request (e.g. { echoCancellation: true }). An explicit deviceId takes precedence over constraints.deviceId
const recorder = useVoiceRecorder({
  deviceId: selectedMicId,
  constraints: { echoCancellation: true },
});

Volume Metering

getVolume() lazily creates an AnalyserNode on first call while recording and tears it down on stop/unmount. Call it from a requestAnimationFrame loop to drive level meters:

useEffect(() => {
  if (!recorder.isRecording) return;
  let raf: number;
  const tick = () => {
    setLevel(recorder.getVolume()); // RMS in [0, 1]
    raf = requestAnimationFrame(tick);
  };
  raf = requestAnimationFrame(tick);
  return () => cancelAnimationFrame(raf);
}, [recorder.isRecording]);

To play back the recorded blob, pair with useObjectUrl.

toAppError

Convert Error | null from hook returns to the AppError shape expected by UI components.

import { toAppError } from '@localmode/react';
import type { AppError } from '@localmode/react';

// In a hook's return statement:
return {
  error: toAppError(error),          // { message: '...', recoverable: true } or null
  error: toAppError(error, false),   // { message: '...', recoverable: false }
};

All @localmode/react hooks return Error | null. Components typically render error.message and check error.recoverable. toAppError bridges the gap:

// Without toAppError (verbose):
error: error ? { message: error.message, recoverable: true } : null

// With toAppError (clean):
error: toAppError(error)

When the source error is a core LocalModeError, its code is carried over to AppError.code and its hint is appended to the message:

// LocalModeError('Model failed', 'MODEL_LOAD_ERROR', { hint: 'Check your network' })
toAppError(error)
// → { message: 'Model failed — Check your network', code: 'MODEL_LOAD_ERROR', recoverable: true }

AppError Type

interface AppError {
  message: string;
  code?: string;
  recoverable?: boolean;
}

useModelLoad

Own the full provider-model load lifecycle: construct the model once per key, normalize download progress across all LocalMode providers into one 0–1 aggregate, and drive a status machine from a warmup inference — the only reliable "fully loaded" signal, since providers load lazily on first inference.

State lives in a module-level registry (consumed via useSyncExternalStore), so progress survives unmount/remount mid-download and multiple components can observe the same load.

import { useModelLoad } from '@localmode/react';
import { transformers, isModelCached } from '@localmode/transformers';

const MODEL_ID = 'Xenova/all-MiniLM-L6-v2';

function SearchPanel() {
  const { status, progressValue, model, load, error } = useModelLoad({
    key: MODEL_ID,
    create: (onProgress) => transformers.embedding(MODEL_ID, { onProgress }),
    isCached: () => isModelCached(MODEL_ID),
  });

  if (status === 'error') return <p>Failed: {error?.message}</p>;
  if (status !== 'ready') {
    return (
      <button onClick={load} disabled={status === 'loading'}>
        Load model ({(progressValue.percent * 100).toFixed(0)}%)
      </button>
    );
  }
  return <SearchView model={model} />;
}

The create-factory pattern

Providers bind onProgress at model construction, so the hook owns the progress callback and your create factory wires it in. The factory is invoked at most once per key, and only on the client (never during SSR):

// transformers
create: (onProgress) => transformers.embedding(id, { onProgress })
// webllm
create: (onProgress) => webllm.languageModel(id, { onProgress })
// wllama
create: (onProgress) => wllama.languageModel(id, { onProgress })

Progress events from every provider shape (transformers per-file bytes, webllm aggregate percent, wllama/litert single-file bytes) are normalized into progress (0–1) and the perFile map. The published progress / progressValue.percent is non-decreasing within a load attempt: when a provider discovers additional files mid-download the raw Σloaded/Σtotal can dip, so the aggregate holds its high-water mark (reset on each load() attempt) while perFile keeps the raw byte counts.

Options

Prop

Type

Return Value

Prop

Type

Pairing progressValue with a download UI

progressValue is shaped to match download-progress UI components directly — for example the DownloadProgress element from the @localmode/ui registry accepts it as its value prop with no adapter:

// installed via: npx shadcn add @localmode/ui/local-first/model-downloader
import { DownloadProgress } from '@/components/ui/model-downloader';

const { progressValue } = useModelLoad({ key, create });

<DownloadProgress value={progressValue} />

Not abortable in v1

load() cannot be cancelled — provider model loads do not accept an AbortSignal, so an in-flight load runs to completion (or failure). Treat the load as a commit once started.

useModelStatus

Read-only view of whether a model is ready for inference. Backed by the same registry as useModelLoad: it looks up the shared load lifecycle by model.modelId and reflects the real state — isLoading while a load is in flight, isReady once the warmup inference resolved, progress (0–1) from normalized provider progress events (non-decreasing within a load attempt, like useModelLoad), and error when the load failed.

import { useModelStatus } from '@localmode/react';

const model = transformers.embedding('Xenova/all-MiniLM-L6-v2');

function StatusBadge() {
  const { isReady, isLoading, progress, error } = useModelStatus(model);

  if (isLoading) return <p>Loading {(progress * 100).toFixed(0)}%...</p>;
  if (error) return <p>Failed to load: {error.message}</p>;
  return <p>{isReady ? 'Model ready' : 'Not loaded'}</p>;
}

Corrected semantics

useModelStatus previously reported isReady: true optimistically as soon as a model instance existed — but provider models load lazily on first inference, so a constructed instance proves nothing. It now observes the actual lifecycle driven by useModelLoad({ key: model.modelId, ... }). When no load has been observed for the modelId, it reports { isReady: false, isLoading: false, progress: 0, error: null }.

useCapabilities

Detect browser AI capabilities on mount. Returns a typed DeviceCapabilities (with browser, device, hardware, features, and storage sub-objects), a detection error, and a refresh() function to re-detect.

import { useCapabilities } from '@localmode/react';

function Demo() {
  const { capabilities, isDetecting, error, refresh } = useCapabilities();

  if (isDetecting) return <p>Detecting...</p>;
  if (error) return <button onClick={refresh}>Retry detection</button>;

  return (
    <ul>
      <li>WebGPU: {capabilities?.features.webgpu ? 'Yes' : 'No'}</li>
      <li>WASM: {capabilities?.features.wasm ? 'Yes' : 'No'}</li>
      <li>IndexedDB: {capabilities?.features.indexedDB ? 'Yes' : 'No'}</li>
    </ul>
  );
}

useNetworkStatus

Reactively track online/offline status.

import { useNetworkStatus } from '@localmode/react';

function Demo() {
  const { isOnline, isOffline } = useNetworkStatus();

  return <p>{isOnline ? 'Online' : 'Offline'}</p>;
}

useStorageQuota

Monitor browser storage usage. Exposes the full core StorageQuota (usedBytes, quotaBytes, percentUsed, isPersisted, availableBytes) — or null while unavailable — plus loading state, the query error, and refresh().

import { useStorageQuota } from '@localmode/react';

function Demo() {
  const { quota, isLoading, error, refresh } = useStorageQuota();

  if (error) return <p>Quota unavailable: {error.message}</p>;

  return (
    <div>
      {quota && (
        <p>
          {quota.percentUsed.toFixed(1)}% used
          ({(quota.availableBytes / 1024 / 1024).toFixed(0)} MB free)
          {quota.isPersisted ? ' — persisted' : ''}
        </p>
      )}
      <button onClick={refresh}>Refresh</button>
    </div>
  );
}

Helper Utilities

Browser utility functions commonly needed when building AI-powered React apps.

readFileAsDataUrl

Read a browser File as a data URL string for passing to image/audio models.

import { readFileAsDataUrl } from '@localmode/react';

const handleFile = async (file: File) => {
  const dataUrl = await readFileAsDataUrl(file);
  await captioner.execute(dataUrl);
};

validateFile

Validate file type and size before processing. Returns AppError | null.

import { validateFile } from '@localmode/react';

const error = validateFile({
  file,
  accept: ['image/png', 'image/jpeg', 'image/webp'],
  maxSize: 10_000_000, // 10MB
});

if (error) {
  setError(error); // { message: '...', recoverable: true }
  return;
}

Options

OptionTypeDescription
fileFileThe file to validate (required)
acceptstring[]Accepted MIME types
maxSizenumberMaximum size in bytes

downloadBlob

Trigger a file download from in-memory content.

import { downloadBlob } from '@localmode/react';

// Download text content
downloadBlob('transcript text...', 'transcript.txt');

// Download binary content
downloadBlob(audioBlob, 'recording.webm', 'audio/webm');

useObjectUrl

Derive a stable object URL from a Blob with automatic URL.revokeObjectURL when the blob changes or the component unmounts. Returns null when the blob is null/undefined, during SSR, and for the first render after a blob change (the URL is created in an effect so server and client markup match).

import { useObjectUrl } from '@localmode/react';

function NotePlayback({ audio }: { audio: Blob | null }) {
  const src = useObjectUrl(audio);
  return src ? <audio controls src={src} /> : null;
}

Pairs naturally with useVoiceRecorder (recorded blobs) and useSynthesizeSpeech (synthesized audio).

useModelLoader

Wraps createModelLoader() from @localmode/core with React state for downloading model files directly from URLs. Use this for custom ONNX models, self-hosted models, or other direct file downloads — not for models loaded through @localmode/transformers or @localmode/webllm (those providers manage their own caching; use useModelLoad for those).

import { useModelLoader } from '@localmode/react';

function ModelManager() {
  const { downloads, isDownloading, error, prefetch, cancel, evict } = useModelLoader({
    maxCacheSize: '2GB',
  });

  if (error) return <p>Loader unavailable: {error.message}</p>; // e.g. IndexedDB blocked

  return (
    <button onClick={() => prefetch([{
      url: 'https://your-cdn.com/models/custom-model.onnx',
      modelId: 'custom-model'
    }])}>
      Download Model
    </button>
  );
}

The error field surfaces loader initialization failures (e.g. IndexedDB blocked in private browsing) instead of silently no-oping. For full API reference and when-to-use guidance, see the Model Cache documentation.

useInferenceQueue

Wraps createInferenceQueue() for priority-based task scheduling with live stats.

import { useInferenceQueue } from '@localmode/react';

function QueueDemo() {
  const { queue, stats, isProcessing } = useInferenceQueue({
    concurrency: 1,
    priorities: ['interactive', 'background'],
  });

  const handleSearch = async (query: string) => {
    const result = await queue.add(
      () => embed({ model, value: query }),
      { priority: 'interactive' }
    );
  };

  return (
    <div>
      {stats && <p>Pending: {stats.pending}, Active: {stats.active}</p>}
    </div>
  );
}

For full API reference, see the Inference Queue documentation.

useSemanticCache

Manages a SemanticCache lifecycle in React components. Creates the cache on mount and destroys it on unmount.

import { useSemanticCache } from '@localmode/react';
import { transformers } from '@localmode/transformers';

function CachedApp() {
  const { cache, stats, isLoading } = useSemanticCache({
    embeddingModel: transformers.embedding('Xenova/bge-small-en-v1.5'),
    threshold: 0.92,
    maxEntries: 100,
  });

  if (isLoading || !cache) return <p>Initializing cache...</p>;

  return (
    <div>
      <p>Entries: {stats.entries}</p>
      <p>Hit rate: {(stats.hitRate * 100).toFixed(1)}%</p>
    </div>
  );
}

For full API reference, see the Semantic Cache documentation.

useAuditLog

Wraps a @localmode/core AuditLog instance (append-only, hash-chained, signed). Loads entries on mount, exposes append/verify/refresh, and retains the most recent verification result as lastVerification.

import { useAuditLog } from '@localmode/react';
import type { AuditLog } from '@localmode/core';

function AuditPanel({ log }: { log: AuditLog }) {
  const { entries, append, verify, lastVerification, isLoading, error } = useAuditLog(log);

  return (
    <div>
      {entries.map((e) => <div key={e.id}>{e.kind}</div>)}
      <button onClick={() => append('user.click', {})}>Log click</button>
      <button onClick={verify}>Verify chain</button>
      {lastVerification && <p>{lastVerification.ok ? 'Chain intact' : `Chain broken at #${lastVerification.brokenAt}`}</p>}
    </div>
  );
}

Return Value

PropertyTypeDescription
entriesAuditEntry[]Entries in chain order, oldest first
isLoadingbooleanTrue while the initial load or refresh is in flight
errorError | nullLast error from append/verify/refresh
append(kind, payload) => Promise<AuditEntry>Append an entry; updates entries on success
verify() => Promise<AuditLogVerifyResult>Run verifyChain against the log
lastVerificationAuditLogVerifyResult | nullResult of the most recent resolved verify() call — { ok, brokenAt?, reason?, entriesChecked, durationMs } (null until one resolves; unchanged when verify() throws)
refresh() => Promise<void>Re-read entries from storage

There is intentionally no clear — the audit log is append-only. For full API reference, see the Audit Log documentation.

useEncryptedVault

Passphrase-locked, encrypted CRUD vault over a pluggable core StorageAdapter (default: a dedicated IndexedDBStorage('vault_<name>')). Items are AES-GCM envelopes at rest (fresh 12-byte IV per write, versioned, base64) — everything except the item id and timestamps is ciphertext. The key is derived once per unlock via core deriveEncryptionKey (PBKDF2) and held only in a ref while unlocked; the passphrase is never retained and no key material is ever persisted. lock() and unmount clear it.

A single unlock(passphrase) entrypoint initializes the vault on first use and verifies the passphrase afterwards — wrong passphrases are detected deterministically via an encrypted verifier record, which works even on an empty vault.

See it live

The Encrypted Vault block drives useEncryptedVault end-to-end in the browser — it runs the full passphrase lifecycle (create / lock / unlock), encrypted note + document CRUD with ciphertext at rest in IndexedDB, and a tamper-evident hash-chained audit log, all built on the ui/security-privacy/passphrase-gate, vault-item-card, and lock-status-badge primitives.

import { useEncryptedVault } from '@localmode/react';

function Vault() {
  const { status, items, error, unlock, lock, createItem, deleteItem } =
    useEncryptedVault<{ note: string }>({ name: 'notes' });

  if (status !== 'unlocked') {
    return (
      <form onSubmit={(e) => { e.preventDefault(); unlock(passphraseInput); }}>
        <button>{status === 'uninitialized' ? 'Create vault' : 'Unlock'}</button>
        {error?.name === 'VaultPassphraseError' && <p>Wrong passphrase</p>}
      </form>
    );
  }
  return (
    <div>
      {items.map((i) => (
        <div key={i.id}>
          {i.data.note} <button onClick={() => deleteItem(i.id)}>x</button>
        </div>
      ))}
      <button onClick={() => createItem({ note: 'hello' })}>Add</button>
      <button onClick={lock}>Lock</button>
    </div>
  );
}

Options

OptionTypeDefaultDescription
namestring'default'Vault namespace — names the default IndexedDB database (vault_<name>) and the vault's storage collection
storageStorageAdapterPluggable storage adapter (e.g. MemoryStorage, Dexie/idb/localForage adapters). When omitted, a dedicated IndexedDBStorage is created, opened, and closed by the hook
iterationsnumber100000PBKDF2 iterations used when the vault is initialized (first unlock). Subsequent unlocks always use the iteration count persisted in the vault meta record

Return Value

PropertyTypeDescription
status'uninitialized' | 'locked' | 'unlocked'Vault lifecycle state, detected from the persisted meta record on mount
itemsVaultItem<T>[]Decrypted item list while unlocked; [] otherwise
isBusybooleanTrue while an unlock/CRUD/refresh operation is in flight
errorError | nullLast operation error — VaultPassphraseError on wrong passphrase, VaultLockedError on CRUD while locked
unlock(passphrase) => Promise<boolean>Unlock the vault — initializes on first use, verifies the passphrase afterwards. Resolves true on success
lock() => voidSynchronously lock: clears the in-memory key and decrypted items
createItem(data) => Promise<VaultItem<T> | null>Encrypt and persist a new item. Resolves null on failure or while locked
readItem(id) => Promise<VaultItem<T> | null>Read and decrypt a single item by id
updateItem(id, data) => Promise<VaultItem<T> | null>Re-encrypt an existing item with new data
deleteItem(id) => Promise<boolean>Delete an item by id. Resolves false if missing or locked
refresh() => Promise<void>Re-read and decrypt all items from storage into items
cancel() => voidAbort the in-flight operation (silent — no error state)

Methods never throw to the caller — failures resolve null/false and surface via error. Encryption uses the Web Crypto API only, so the vault requires a secure context (HTTPS or localhost). One vault per storage namespace: give each vault its own adapter (the default IndexedDB database is already namespaced per vault name). For the underlying primitives, see the Encryption documentation.

Blocks

AppDescriptionLinks
Chat (LLM Chat)Uses toAppError for error handling, downloadBlob for exportsLive block · Source
Knowledge Base (Semantic Search)Uses toAppError, downloadBlob, validateFileLive block · Source
Audio Studio (Voice Notes)Uses toAppError for transcription error handlingLive block · Source
Vision (Object Detector)Uses readFileAsDataUrl for image loadingLive block · Source
Privacy (PII Redactor)Uses toAppError and downloadBlobLive block · Source
Text Insights (Model Evaluator)Uses toAppError for evaluation error handlingLive block · Source

On this page