# React 绑定

> CardProvider 以及 @neurosquad/card-sdk/react 的各种 hook——上下文、设置、存储、智能体、端口、工具、主题、语言和可见性。

Source: https://docs.neurosquad.ai/zh/card-sdk/react

`@neurosquad/card-sdk/react` 用一个 provider 包裹卡片，并把它的实时状态以 hook 的形式提供出来。
React 18.2+ 或 19 是对等依赖（peer dependency）；React 模板已经把一切配置好了。

```tsx
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`

| 属性 | 含义 |
| --- | --- |
| `card` | 你自己连接好的 card——例如来自[模拟宿主](https://docs.neurosquad.ai/zh/card-sdk/testing)。不提供时，provider 会调用 `connect()`。 |
| `connectOptions` | 传给 `connect()` 的选项。 |
| `fallback` | 连接期间渲染的内容。 |
| `errorFallback` | `(error) => ReactNode`，连接失败时渲染。默认显示错误消息。 |

React 模板会先完成连接，再传入 `card`——这样同一个入口既能在应用中运行，也能配合模拟宿主在浏览器中预览。

## Hook

| Hook | 返回 |
| --- | --- |
| `useCard()` | [`Card`](https://docs.neurosquad.ai/zh/card-sdk/api#the-card-object)——用于没有专门 hook 的一切操作。 |
| `useCardContext()` | 实时的 [`HostContext`](https://docs.neurosquad.ai/zh/card-sdk/api#context)；任何变化都会重新渲染。 |
| `useSettings()` | `[settings, setSettings]`——`settings.values`、`settings.secrets`；setter 修改非密钥值。 |
| `useStorage(key, initial, { scope? })` | `[value, setValue, { loading, error }]`——和 `useState` 一样，但会持久化。setter 立即更新并在后台写入；也接受函数。`scope: 'package'` 会跟随卡片其他副本的写入。 |
| `useAgents()` | `{ agents, loading, error, refresh }`——随状态和变更事件保持最新。需要 `agents.read`。 |
| `useAgentStatus(agentId)` | 某个智能体的状态，或 `undefined`。 |
| `usePort(input?)` | `{ data, message }`——某个输入（或任意输入）最后收到的值。 |
| `useEmit(output)` | 一个稳定的 `(data) => Promise<number>`，用于在某个输出上发送。 |
| `usePortRequest(input, handler)` | 挂载期间应答某个请求型输入。 |
| `usePeers()` | 相连的卡片及其端口，实时更新。 |
| `useTool(name, handler)` | 挂载期间实现清单中的某个工具；总是调用最新的处理函数。 |
| `useCardEvent(event, handler)` | 挂载期间监听任意事件；总是调用最新的处理函数。 |
| `useTheme()` | 实时的 `ThemeSnapshot`（CSS 变量已经应用好了）。 |
| `useLanguage()` | 实时的 `{ language, locale }`。 |
| `useTranslator(catalog)` | 一个[翻译函数](https://docs.neurosquad.ai/zh/card-sdk/api/environment#i18n)，切换语言时会重新渲染。请在组件外定义 catalog。 |
| `useVisibility()` | `visible`、`offscreen`、`overview` 或 `hidden`。 |
| `usePaused()` | 没人能看到卡片正文时为 `true`——此时暂停动画和轮询。 |
| `useExpanded()` | 卡片是否已展开。 |

## 更完整的示例

```tsx
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>
  )
}
```

> 会调用应用的 hook（`useAgents`、`useStorage`）不会抛出异常，而是在它们的 `error` 字段中报告失败——
> 缺少权限时，那里会出现一个带 `PERMISSION_DENIED` 的 `CardSdkError`。

## 使用本地 SDK 副本

如果卡片通过 `file:` 链接依赖 SDK，npm 会创建符号链接，Vite 可能在 SDK 旁边加载第二份 React——此时 hook 会报 “Invalid hook call”。
React 模板的 `vite.config.ts` 已经包含 `resolve: { dedupe: ['react', 'react-dom'] }`；在你自己的配置中请加上它，或在卡片的 `.npmrc` 中设置 `install-links=true`。
