---
title: React Hooks
description: >-
  IvyProvider, useIvyJob, and useIvyWebSocket hooks for React integration
---

The `/react` subpath lets you drop IvyAI into a React app without wiring the client by hand — a provider supplies the client through Context, and hooks handle job submission and live WebSocket progress for you. Reach for it when you're building React UI (like a dashboard) and want state-aware hooks instead of calling the [client](/docs/api/sdk-client) methods directly. It wraps the same underlying pieces documented across the other SDK pages, so it sits one layer above them.

## SDK React Hooks Reference

> **Planned specification.** `@ivybiosciences/sdk` is not released, and there is no package to install. This page describes the SDK's planned interface. To call the platform today, use the [REST API](/docs/api/rest-jobs), the [ivybloom CLI](/docs/cli), or the [MCP server](/docs/cli/mcp).

The `@ivybiosciences/sdk/react` subpath provides React hooks for integrating IvyAI into React applications. Includes context management, job submission, and WebSocket subscriptions.

```ts
import { IvyProvider, useIvyClient, useIvyJob, useIvyWebSocket } from "@ivybiosciences/sdk/react"
```

> React 18+ is a peer dependency — it's only required if you import from the `/react` subpath.

---

## `IvyProvider`

Provides an `IvyClient` instance to the component tree via React Context. All hooks require a parent ````.

```tsx
function IvyProvider(props: { config: IvyClientConfig; children: ReactNode }): JSX.Element
```

The client is memoized — it's only re-created when `config.apiKey` or `config.baseUrl` change.

### Examples

```tsx
// With Clerk JWT authentication
import { IvyProvider } from "@ivybiosciences/sdk/react"
import { useAuth } from "@clerk/nextjs"

function App() {
  const { getToken } = useAuth()
  return (
    <IvyProvider config={{ getToken: () => getToken({ template: "ivy_api" }) }}>
      <YourApp />
    </IvyProvider>
  )
}
```

```tsx
// With API key (e.g., in Electron or internal tools)
<IvyProvider config={{ apiKey: process.env.IVY_API_KEY }}>
  <Dashboard />
</IvyProvider>
```

---

## `useIvyClient()`

Access the `IvyClient` instance from context.

```ts
function useIvyClient(): IvyClient
```

**Throws:** `Error` if called outside of an ````.

### Example

```tsx
function MyComponent() {
  const ivy = useIvyClient()
  const handleClick = async () => {
    const jobs = await ivy.jobs.list({ limit: 5 })
    console.log(jobs)
  }
  return <button onClick={handleClick}>List Jobs</button>
}
```

---

## `useIvyJob(options?)`

Submit and track computational jobs with real-time progress updates.

```ts
function useIvyJob(options?: UseIvyJobOptions): UseIvyJobReturn
```

Combines job submission with real-time progress tracking. Handles WebSocket subscription, polling fallback, error states, and cleanup on unmount.

### Lifecycle

1. Call `submit(toolName, params)` to start a job
2. `isLoading` becomes `true`, `status` updates in real-time
3. On completion: `onComplete` fires, `isLoading` becomes `false`
4. On error: `onError` fires, `error` is populated

### Options

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `onProgress` | `(progress: JobProgress) => void` | — | Called on each status/progress change |
| `onComplete` | `(result: JobProgress) => void` | — | Called when job reaches COMPLETED status |
| `onError` | `(error: Error) => void` | — | Called on submission or runtime failures |
| `useWebSocket` | `boolean` | `true` | Use WebSocket for real-time updates |
| `pollInterval` | `number` | `3000` | Polling interval in ms (fallback mode) |

### Return Value

| Field | Type | Description |
|-------|------|-------------|
| `submit` | `(toolName, parameters, opts?) => Promise<string>` | Submit a job and begin tracking |
| `status` | `JobProgress \| null` | Current progress snapshot |
| `isLoading` | `boolean` | Whether a job is in progress |
| `error` | `Error \| null` | Last error encountered |
| `cancel` | `() => void` | Cancel tracking (not server-side computation) |

### Examples

```tsx
// Basic protein folding
function ProteinFolder() {
  const { submit, status, isLoading, error } = useIvyJob({
    onComplete: (result) => console.log("Done!", result),
  })

  return (
    <button onClick={() => submit("esmfold", { protein_sequence: "MKFLI..." })}>
      {isLoading ? `Folding... ${status?.progress ?? 0}%` : "Fold Protein"}
    </button>
  )
}
```

```tsx
// With progress display and cancel button
function JobRunner() {
  const { submit, status, isLoading, error, cancel } = useIvyJob({
    onProgress: (p) => console.log(`[${p.phase}] ${p.progress}%`),
    onError: (e) => toast.error(e.message),
  })

  return (
    <div>
      {status && <ProgressBar value={status.progress ?? 0} />}
      {isLoading && <button onClick={cancel}>Cancel</button>}
      {error && <p className="error">{error.message}</p>}
    </div>
  )
}
```

---

## `useIvyWebSocket(jobId, options?)`

Subscribe to real-time WebSocket updates for a specific job. Use this when you need fine-grained control over WebSocket subscriptions. For most use cases, `useIvyJob` (which uses WebSocket internally) is simpler.

```ts
function useIvyWebSocket(
  jobId: string | null,
  options?: UseIvyWebSocketOptions,
): UseIvyWebSocketReturn
```

Manages the full WebSocket lifecycle declaratively:
- Connects when `jobId` is provided and `enabled` is true
- Disconnects and cleans up on unmount or when `jobId` changes
- Provides reactive state for progress, connection status, and errors

### Options

Extends `WebSocketOptions` with:

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `enabled` | `boolean` | `true` | Whether to establish the connection |
| `autoReconnect` | `boolean` | `true` | Reconnect on unexpected close |
| `maxReconnectAttempts` | `number` | `5` | Max reconnection attempts |
| `reconnectInterval` | `number` | `1000` | Base delay in ms (doubles each attempt) |
| `onReconnect` | `(attempt: number) => void` | — | Called on each reconnection attempt |
| `onClose` | `(code: number, reason: string) => void` | — | Called when connection closes |

### Return Value

| Field | Type | Description |
|-------|------|-------------|
| `status` | `JobProgress \| null` | Latest progress data from WebSocket |
| `isConnected` | `boolean` | Whether the WebSocket is in OPEN state |
| `error` | `Error \| null` | Connection error, if any |

### Examples

```tsx
// Basic job monitoring
function JobMonitor({ jobId }: { jobId: string }) {
  const { status, isConnected, error } = useIvyWebSocket(jobId)

  if (error) return <p>Connection error: {error.message}</p>
  if (!isConnected) return <p>Connecting...</p>

  return (
    <div>
      <p>Status: {status?.status}</p>
      <p>Progress: {status?.progress ?? 0}%</p>
      <p>Phase: {status?.phase ?? "initializing"}</p>
    </div>
  )
}
```

```tsx
// Conditional connection (only connect when jobId is available)
function ConditionalMonitor({ jobId }: { jobId: string | null }) {
  const { status } = useIvyWebSocket(jobId, { enabled: !!jobId })
  // Only connects when jobId is non-null
}
```

---

## Hook Comparison

| Hook | Auth Required | WebSocket | Use Case |
|------|:---:|:---:|------|
| `useIvyJob` | Yes | Automatic | Full job lifecycle (submit + track) |
| `useIvyWebSocket` | Yes | Manual | Custom monitoring of existing jobs |

Every hook needs an API key or a Clerk JWT. To try the platform without a key, send the keyless orchestrate request in [Guest curl](/docs/api#guest-curl) instead.

---

## Full Example: Job Dashboard

```tsx
import { IvyProvider, useIvyJob, useIvyWebSocket } from "@ivybiosciences/sdk/react"
import { useAuth } from "@clerk/nextjs"

function App() {
  const { getToken } = useAuth()
  return (
    <IvyProvider config={{ getToken: () => getToken({ template: "ivy_api" }) }}>
      <JobDashboard />
    </IvyProvider>
  )
}

function JobDashboard() {
  const { submit, status, isLoading, error, cancel } = useIvyJob({
    onComplete: (result) => {
      if (result.presignedUrl) {
        window.open(result.presignedUrl) // Download artifact
      }
    },
    onError: (e) => alert(`Job failed: ${e.message}`),
  })

  const handleSubmit = () => {
    submit("esmfold", {
      protein_sequence: "MKFLILLFNILCLFPVLAADNH",
    }, {
      analysisLevel: "comprehensive",
    })
  }

  return (
    <div>
      <button onClick={handleSubmit} disabled={isLoading}>
        {isLoading ? "Running..." : "Run ESMFold"}
      </button>

      {status && (
        <div>
          <p>Phase: {status.phase}</p>
          <progress value={status.progress ?? 0} max={100} />
          <p>{status.progress ?? 0}%</p>
        </div>
      )}

      {isLoading && <button onClick={cancel}>Cancel</button>}
      {error && <p style={{ color: "red" }}>{error.message}</p>}
    </div>
  )
}
```

---

## Advanced Patterns

### Combining Hooks: Multi-Job Monitoring

When you need to track multiple jobs simultaneously, combine `useIvyClient` for submission with `useIvyWebSocket` for per-job monitoring.

```tsx
import { useIvyClient, useIvyWebSocket } from "@ivybiosciences/sdk/react"
import { useState } from "react"

interface TrackedJob {
  id: string
  toolName: string
  submittedAt: Date
}

function MultiJobDashboard() {
  const ivy = useIvyClient()
  const [jobs, setJobs] = useState<TrackedJob[]>([])

  const submitAndTrack = async (toolName: string, params: Record<string, unknown>) => {
    // Submit via client directly for fine-grained control
    const { id } = await ivy.jobs.submit({
      toolName,
      parameters: params,
      analysisLevel: "standard",
    })

    // Add to tracked jobs list
    setJobs((prev) => [...prev, { id, toolName, submittedAt: new Date() }])
  }

  return (
    <div>
      <button onClick={() => submitAndTrack("esmfold", { protein_sequence: "MKFLI..." })}>
        Add ESMFold Job
      </button>
      <button onClick={() => submitAndTrack("admet_ai", { smiles: "CCO" })}>
        Add ADMET Job
      </button>

      <div className="grid grid-cols-2 gap-4 mt-4">
        {jobs.map((job) => (
          <JobCard key={job.id} job={job} />
        ))}
      </div>
    </div>
  )
}

// Each card has its own WebSocket subscription
function JobCard({ job }: { job: TrackedJob }) {
  const { status, isConnected } = useIvyWebSocket(job.id)

  const isDone = status?.status === "COMPLETED" || status?.status === "FAILURE"

  return (
    <div className="border rounded p-4">
      <h3>{job.toolName}</h3>
      <p className="text-xs text-gray-500">
        {job.submittedAt.toLocaleTimeString()}
      </p>

      {!isConnected && !isDone && <p>Connecting...</p>}

      {status && (
        <>
          <progress value={status.progress ?? 0} max={100} className="w-full" />
          <p>{status.phase} — {status.progress ?? 0}%</p>
        </>
      )}

      {status?.status === "COMPLETED" && (
        <p className="text-green-600">Complete</p>
      )}
      {status?.status === "FAILURE" && (
        <p className="text-red-600">{status.error}</p>
      )}
    </div>
  )
}
```

> **Why this pattern?** `useIvyJob` manages a single job's lifecycle. When monitoring multiple concurrent jobs, using `useIvyClient` for submission + individual `useIvyWebSocket` subscriptions gives you independent state per job without conflicts.

---

### State Management Integration

Integrating IvyAI hooks with external state managers (Zustand, Redux, etc.) for persistence and cross-component sharing.

```tsx
import { create } from "zustand"
import { persist } from "zustand/middleware"
import { useIvyClient } from "@ivybiosciences/sdk/react"
import type { JobProgress } from "@ivybiosciences/sdk"

// Zustand store for job history persistence
interface JobStore {
  completedJobs: Array<{ id: string; result: unknown; completedAt: string }>
  addCompletedJob: (id: string, result: unknown) => void
  clearHistory: () => void
}

const useJobStore = create<JobStore>()(
  persist(
    (set) => ({
      completedJobs: [],
      addCompletedJob: (id, result) =>
        set((state) => ({
          completedJobs: [
            { id, result, completedAt: new Date().toISOString() },
            ...state.completedJobs.slice(0, 49), // Keep last 50
          ],
        })),
      clearHistory: () => set({ completedJobs: [] }),
    }),
    { name: "ivy-job-history" }
  )
)

function PersistentJobRunner() {
  const ivy = useIvyClient()
  const { addCompletedJob, completedJobs } = useJobStore()
  const [currentJobId, setCurrentJobId] = useState<string | null>(null)

  const runJob = async () => {
    const { id } = await ivy.jobs.submit({
      toolName: "esmfold",
      parameters: { protein_sequence: "MKFLI..." },
    })
    setCurrentJobId(id)
  }

  // When job completes, persist to store
  const handleComplete = (result: JobProgress) => {
    if (currentJobId && result.result) {
      addCompletedJob(currentJobId, result.result)
      setCurrentJobId(null)
    }
  }

  return (
    <div>
      <button onClick={runJob}>Run Job</button>

      {currentJobId && (
        <ActiveJobMonitor jobId={currentJobId} onComplete={handleComplete} />
      )}

      <h3>History ({completedJobs.length} jobs)</h3>
      <ul>
        {completedJobs.map((job) => (
          <li key={job.id}>{job.id} — {job.completedAt}</li>
        ))}
      </ul>
    </div>
  )
}
```

---

### Error Recovery with Retry Logic

Build resilient UIs with automatic retry on transient failures.

```tsx
import { useIvyJob, IvyApiError } from "@ivybiosciences/sdk/react"
import { useState, useRef } from "react"

function ResilientJobRunner() {
  const [retryCount, setRetryCount] = useState(0)
  const maxRetries = 3
  const lastParamsRef = useRef<{ tool: string; params: Record<string, unknown> } | null>(null)

  const { submit, status, isLoading, error } = useIvyJob({
    onError: (err) => {
      // Only auto-retry on transient errors
      const isRetryable =
        err instanceof IvyApiError &&
        (err.isRateLimited || err.isServerError)

      if (isRetryable && retryCount < maxRetries && lastParamsRef.current) {
        const delay = Math.pow(2, retryCount) * 1000 // Exponential backoff
        console.log(`Retrying in ${delay}ms (attempt ${retryCount + 1}/${maxRetries})`)

        setTimeout(() => {
          setRetryCount((c) => c + 1)
          const { tool, params } = lastParamsRef.current!
          submit(tool, params).catch(() => {}) // Fire and forget, error handled by hook
        }, delay)
      }
    },
    onComplete: () => {
      // Reset retry counter on success
      setRetryCount(0)
      lastParamsRef.current = null
    },
  })

  const handleSubmit = async (tool: string, params: Record<string, unknown>) => {
    lastParamsRef.current = { tool, params }
    setRetryCount(0)
    await submit(tool, params)
  }

  return (
    <div>
      <button
        onClick={() => handleSubmit("esmfold", { protein_sequence: "MKFLI..." })}
        disabled={isLoading}
      >
        {isLoading ? "Running..." : "Predict Structure"}
      </button>

      {retryCount > 0 && (
        <p className="text-yellow-600">
          Retry attempt {retryCount}/{maxRetries}...
        </p>
      )}

      {error && retryCount >= maxRetries && (
        <p className="text-red-600">
          Failed after {maxRetries} retries: {error.message}
        </p>
      )}
    </div>
  )
}
```

---

## Best Practices

### Provider Placement

Place ```` as high as needed, but not higher. If only one route uses IvyAI, wrap just that route:

```tsx
// Good: Scoped provider
function DrugDiscoveryPage() {
  return (
    <IvyProvider config={config}>
      <DrugDiscoveryTools />
    </IvyProvider>
  )
}

// Avoid: Global provider when not needed everywhere
function App() {
  return (
    <IvyProvider config={config}>
      <HomePage />        {/* Doesn't use IvyAI */}
      <AboutPage />       {/* Doesn't use IvyAI */}
      <DrugDiscoveryPage /> {/* Only page that uses IvyAI */}
    </IvyProvider>
  )
}
```

### Handle Loading States Gracefully

Always provide visual feedback during async operations:

```tsx
function GoodLoadingUX() {
  const { submit, status, isLoading, error } = useIvyJob()

  return (
    <div>
      <button disabled={isLoading}>
        {isLoading ? (
          <>
            <Spinner className="mr-2" />
            {status?.phase ?? "Starting"}...
          </>
        ) : (
          "Run Analysis"
        )}
      </button>

      {isLoading && status && (
        <div className="mt-2">
          <progress value={status.progress ?? 0} max={100} />
          <p className="text-sm text-gray-500">{status.phase}</p>
        </div>
      )}
    </div>
  )
}
```

### Clean Up on Unmount

The hooks handle cleanup automatically, but be aware of the behavior:

- `useIvyJob`: Calls `cancel()` internally on unmount — stops tracking but **does not cancel server-side computation**
- `useIvyWebSocket`: Closes WebSocket connection on unmount

The SDK has no server-side cancel call. A job keeps running after the component unmounts; delete it with `ivy.jobs.delete(jobId)` if you no longer want it.

### Type Your Results

Define explicit types for job results to get TypeScript benefits:

```tsx
interface ESMFoldResult {
  plddt_mean: number
  num_residues: number
  pdb_content: string
}

function TypedJobHandler() {
  const { submit, status } = useIvyJob({
    onComplete: (result) => {
      // Type assertion after validation
      if (result.result && typeof result.result === "object") {
        const esmfold = result.result as ESMFoldResult
        console.log(`pLDDT: ${esmfold.plddt_mean}`)
      }
    },
  })

  // ...
}
```
