Skip to Content
Card SDKReact bindings

React bindings

@neurosquad/card-sdk/react wraps the card in a provider and exposes its live state as hooks. React 18.2+ or 19 is a peer dependency; the React template sets everything up.

import { createRoot } from 'react-dom/client' import { CardProvider, useCardContext, useStorage } from '@neurosquad/card-sdk/react' function App() { const { instance, workspace } = useCardContext() const [count, setCount] = useStorage('count', 0) return ( <button className="ns-btn ns-btn--primary" onClick={() => setCount(count + 1)}> {instance.displayName} in {workspace.name}: {count} </button> ) } createRoot(document.getElementById('root')!).render( <CardProvider fallback={<p>Connecting…</p>}> <App /> </CardProvider> )

CardProvider

PropMeaning
cardA card you connected yourself — for example from the mock host. Without it the provider calls connect().
connectOptionsOptions for connect().
fallbackRendered while connecting.
errorFallback(error) => ReactNode, rendered if connecting fails. Default: the error message.

The React template connects first and then passes card — so the same entry works inside the app and, with the mock host, in a browser preview.

Hooks

HookReturns
useCard()The Card — for anything without a dedicated hook.
useCardContext()The live HostContext; re-renders on any change.
useSettings()[settings, setSettings] — settings.values, settings.secrets; the setter changes non-secret values.
useStorage(key, initial, { scope? })[value, setValue, { loading, error }] — like useState, persisted. The setter updates at once and writes in the background; accepts a function. scope: 'package' follows writes from other copies of the card.
useAgents(){ agents, loading, error, refresh } — kept current from status and change events. Needs agents.read.
useAgentStatus(agentId)One agent’s status, or undefined.
usePort(input?){ data, message } — the last value that arrived on an input (or any input).
useEmit(output)A stable (data) => Promise<number> that emits on an output.
usePortRequest(input, handler)Answers a request input while mounted.
usePeers()Connected cards and their ports, live.
useTool(name, handler)Implements a manifest tool while mounted; always calls the latest handler.
useCardEvent(event, handler)Any event while mounted; always calls the latest handler.
useTheme()The ThemeSnapshot, live (the CSS variables are applied already).
useLanguage(){ language, locale }, live.
useTranslator(catalog)A translator that re-renders on a language switch. Define the catalog outside the component.
useVisibility()visible, offscreen, overview or hidden.
usePaused()true when nobody can see the card’s body — pause animations and polling.
useExpanded()Whether the card is expanded.

A fuller example

import type { Catalog } from '@neurosquad/card-sdk' import { useAgents, useCard, useEmit, usePaused, usePort, useTool, useTranslator } from '@neurosquad/card-sdk/react' import { useEffect } from 'react' const catalog: Catalog = { en: { waiting_one: '{{count}} agent waits for you', waiting_other: '{{count}} agents wait for you' }, ru: { waiting_one: '{{count}} агент ждёт вас', waiting_few: '{{count}} агента ждут вас', waiting_many: '{{count}} агентов ждут вас', waiting_other: '{{count}} агента ждут вас' }, zh: { waiting_other: '{{count}} 个智能体在等你' } } export function Waiting() { const card = useCard() const t = useTranslator(catalog) const paused = usePaused() const { agents } = useAgents() const waiting = agents.filter((a) => a.status === 'needs-input') const { data: note } = usePort<string>('notes') const emit = useEmit<string>('digest') // Keep the overview tile and attention in step with the data. useEffect(() => { void card.setOverview({ primary: t('waiting', { count: waiting.length }), icon: 'bell' }) void card.attention(waiting.length > 0 ? 'needs-input' : 'none').catch(() => undefined) }, [card, t, waiting.length]) useTool('list_waiting', () => waiting.map((a) => a.name)) return ( <div className="ns-stack"> <p className={paused ? '' : 'pulse'}>{t('waiting', { count: waiting.length })}</p> {note ? <pre className="ns-mono">{note}</pre> : null} <button className="ns-btn" onClick={() => void emit(waiting.map((a) => `- ${a.name}`).join('\n'))}> Send digest </button> </div> ) }

Hooks that call the app (useAgents, useStorage) report failures in their error field instead of throwing — a missing permission shows up there as a CardSdkError with PERMISSION_DENIED.

Using a local SDK checkout

If your card depends on the SDK through a file: link, npm creates a symlink and Vite may load a second copy of React next to the SDK — hooks then fail with “Invalid hook call”. The React template’s vite.config.ts already has resolve: { dedupe: ['react', 'react-dom'] }; in your own setup, add it, or set install-links=true in the card’s .npmrc.