Skip to main content
    Ivy Biosciences Docs/Call the platform/API/SDKs/React Hooks

    React Hooks

    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 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, the ivybloom CLI, or the MCP server.

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

    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 <IvyProvider>.

    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

    // 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>
      )
    }
    // 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.

    function useIvyClient(): IvyClient

    Throws: Error if called outside of an <IvyProvider>.

    Example

    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.

    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

    FieldTypeDefaultDescription
    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
    useWebSocketbooleantrueUse WebSocket for real-time updates
    pollIntervalnumber3000Polling interval in ms (fallback mode)

    Return Value

    FieldTypeDescription
    submit(toolName, parameters, opts?) => Promise<string>Submit a job and begin tracking
    statusJobProgress | nullCurrent progress snapshot
    isLoadingbooleanWhether a job is in progress
    errorError | nullLast error encountered
    cancel() => voidCancel tracking (not server-side computation)

    Examples

    // 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>
      )
    }
    // 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.

    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:

    FieldTypeDefaultDescription
    enabledbooleantrueWhether to establish the connection
    autoReconnectbooleantrueReconnect on unexpected close
    maxReconnectAttemptsnumber5Max reconnection attempts
    reconnectIntervalnumber1000Base 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

    FieldTypeDescription
    statusJobProgress | nullLatest progress data from WebSocket
    isConnectedbooleanWhether the WebSocket is in OPEN state
    errorError | nullConnection error, if any

    Examples

    // 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>
      )
    }
    // 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

    HookAuth RequiredWebSocketUse Case
    useIvyJobYesAutomaticFull job lifecycle (submit + track)
    useIvyWebSocketYesManualCustom 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 instead.


    Full Example: Job Dashboard

    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.

    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.

    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.

    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 <IvyProvider> as high as needed, but not higher. If only one route uses IvyAI, wrap just that route:

    // 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:

    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:

    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}`)
          }
        },
      })
    
      // ...
    }

    Was this page helpful?

    Loading...