---
title: React
description: Build React interfaces for live agent conversations and workflow runs.
---

`@bapX/react` turns Bapx's durable event streams into live React state. Use `useBapxAgent()` for a continuing conversation with an agent instance and `useBapxWorkflow()` to observe a finite workflow run. HTTP requests, authentication, and stream transport remain in `@bapX/sdk`.

## Set up React

Install both packages, create one SDK client, and provide it to your application:

```sh
npm install @bapX/react @bapX/sdk
```

```tsx title="src/main.tsx"
import { BapxProvider } from '@bapX/react';
import { createBapxClient } from '@bapX/sdk';
import { createRoot } from 'react-dom/client';
import { App } from './App.tsx';

const client = createBapxClient({ baseUrl: '/api' });

createRoot(document.getElementById('root')!).render(
  <BapxProvider client={client}>
    <App />
  </BapxProvider>,
);
```

Configure authentication, headers, and custom `fetch` behavior on the client. The agent and workflow modules used below must export `route`; mounting `bapX()` alone does not expose them. See [Routing](/guide/routing/) to expose and protect the Bapx API, including cross-origin applications.

## Build an agent conversation

An agent instance is identified by its agent name and instance ID. The hook reconstructs its transcript from durable events, then follows new events:

```tsx title="src/Chat.tsx"
import { useBapxAgent } from '@bapX/react';
import { useState } from 'react';

export function Chat({ conversationId }: { conversationId: string }) {
  const [input, setInput] = useState('');
  const agent = useBapxAgent({
    name: 'support-assistant',
    id: conversationId,
  });

  async function submit(event: React.FormEvent) {
    event.preventDefault();
    const message = input.trim();
    if (!message) return;

    setInput('');
    await agent.sendMessage(message);
  }

  return (
    <section>
      <div aria-live="polite">
        {agent.messages.map((message) => (
          <article key={message.id}>
            <strong>{message.role}</strong>
            {message.parts.map((part) =>
              part.type === 'text' ? <p key={part.text}>{part.text}</p> : null,
            )}
          </article>
        ))}
      </div>

      <form onSubmit={submit}>
        <input value={input} onChange={(event) => setInput(event.target.value)} />
        <button disabled={!input.trim()} type="submit">
          Send
        </button>
      </form>
    </section>
  );
}
```

`sendMessage()` adds the user message immediately and resolves when the server admits the prompt, not when generation finishes. The stream then reconciles that optimistic message with its durable copy without changing its transcript position. Use `status` to distinguish connection, submission, streaming, and error states. `historyReady` becomes `true` once the requested durable history has loaded as one coherent snapshot; it remains `true` through later live reconnects.

Messages are Bapx-owned `BapxConversationMessage` values with a parts-based shape: `text`, `reasoning`, `dynamic-tool`, and `file`. Validated structured tool output is preserved on the `dynamic-tool` part's `output`, so applications can render custom tool interfaces without a separate data-event channel. These are Bapx's own types — they are not AI SDK types, and `@bapX/react` neither depends on `ai` at runtime nor implements its transport protocol. Durable file attachments project as `file` parts carrying media type only; historical bytes are not served, so render uploads optimistically from local data.

The hook uses the SDK's materialized `agents.observe()` layer: it loads the complete canonical snapshot, publishes it atomically in durable order, and continues from that exact checkpoint through reconnects and canonical resets. Consumers do not need to coordinate the snapshot and live updates or sort `messages`. Observation follows live updates with Durable Streams long-polling. For a single point-in-time read with no live updates, call `client.agents.history()` directly instead. Partial text and reasoning are best-effort while streaming; the completed canonical assistant message is authoritative.

To observe a conversation that may be created out-of-band after mount — by a server-side wakeup, queue worker, or webhook — call `refresh()` to re-run history catch-up and resume live updates. Deciding when to re-check is the application's responsibility, matching the SDK's [`observe()` model](/sdk/agents/#clientagentsobserve).

## Observe a workflow run

Workflow invocation and observation are separate. Invoke the workflow with the SDK, retain its `runId`, and pass that ID to `useBapxWorkflow()`:

```tsx title="src/Report.tsx"
import { useBapxClient, useBapxWorkflow } from '@bapX/react';
import { useState } from 'react';

export function Report() {
  const bapX = useBapxClient();
  const [runId, setRunId] = useState<string>();
  const run = useBapxWorkflow({ runId });

  async function generate() {
    const invocation = await bapX.workflows.invoke('weekly-report', {
      input: { week: 'current' },
    });
    setRunId(invocation.runId);
  }

  return (
    <section>
      <button onClick={generate} type="button">
        Generate report
      </button>
      <p>{run.status}</p>
      {run.logs.map((event) => (
        <pre key={`${event.timestamp}:${event.eventIndex}`}>{event.message}</pre>
      ))}
    </section>
  );
}
```

The hook replays the complete run before following live events, so it can attach before, during, or after execution. `events` contains the full event history, `logs` selects log events, and `result` or `error` records the terminal outcome.

Transport failures reconnect from the last durable checkpoint. A workflow failure instead produces the terminal `errored` status; observation that ends without a terminal workflow event becomes `disconnected`.

## Rendering and deferred identity

During server rendering, both hooks return empty, idle state and open no connections. The SDK client still needs an absolute `baseUrl` when it is created on the server; relative paths such as `/api` are browser-only. An omitted agent `id` or workflow `runId` also leaves the hook dormant while routing or application data resolves the identity.

## API reference

See the [`@bapX/react` package README](https://github.com/getwinharris/agents/tree/main/packages/react#readme) for complete options, result types, statuses, and message-part types.
