Skip to main content
This guide covers the core concepts, best practices, and patterns for building applications with Reactor.

Understanding Reactor

Reactor enables you to build realtime AI applications by connecting your frontend to GPU-powered machines that stream interactive AI experiences. Think of it as a bridge between your web application and powerful AI models running on remote GPUs.

Connection Lifecycle

A Reactor connection goes through four states:
disconnected → connecting → waiting → ready
  1. disconnected: No active connection
  2. connecting: Establishing connection to the Reactor coordinator
  3. waiting: In queue, waiting for a GPU to be assigned
  4. ready: Connected to GPU machine, can send and receive messages

Status Flow Diagram

┌─────────────┐
│ disconnected│
└──────┬──────┘
       │ connect()

┌─────────────┐
│  connecting │
└──────┬──────┘
       │ coordinator connected

┌─────────────┐
│   waiting   │◄─── Queue position updates
└──────┬──────┘
       │ GPU assigned

┌─────────────┐
│    ready    │◄─── Can send/receive messages
└──────┬──────┘
       │ disconnect()

┌─────────────┐
│ disconnected│
└─────────────┘

Queue System

Reactor provides two modes for handling connections when no GPU machines are available, controlled by the queueing flag:

Queueing Enabled (queueing: true)

When queueing is enabled, users are placed in a FIFO (first-in, first-out) queue if no machines are immediately available. During the waiting state, Reactor provides queue information:
  • position: Your place in line
This mode is ideal for applications where you want to ensure all users eventually get access, even during high-demand periods.

Queueing Disabled (queueing: false, default)

When queueing is disabled, users are immediately disconnected if no machines are available. This mode is useful for applications where you prefer to fail fast and let users retry later, or when you want to implement your own custom queueing logic. Examples:
// Imperative API - Enable queueing
const reactor = new Reactor({
  insecureApiKey: process.env.REACTOR_API_KEY,
  modelName: "longlive",
  queueing: true  // Users will be queued
});

// Imperative API - Disable queueing (default)
const reactor = new Reactor({
  insecureApiKey: process.env.REACTOR_API_KEY,
  modelName: "longlive",
  queueing: false  // Users disconnected if no machines available
});

// React API - Enable queueing
<ReactorProvider
  modelName="longlive"
  insecureApiKey={process.env.REACTOR_API_KEY}
  queueing={true}
>
  <App />
</ReactorProvider>

Using the Imperative API

The imperative API gives you direct control over Reactor connections. Use this in vanilla JavaScript/TypeScript applications or when you need fine-grained control.

Basic Setup

import { Reactor } from "@reactor-team/js-sdk";

// Create instance
const reactor = new Reactor({
  insecureApiKey: process.env.REACTOR_API_KEY,
  modelName: "longlive"
});

// Set up video display
const videoElement = document.getElementById("videoStream") as HTMLVideoElement;
reactor.on("streamChanged", (videoTrack) => {
  if (videoTrack) {
    videoTrack.attach(videoElement);
  } else {
    videoElement.srcObject = null;
  }
});

// Connect
await reactor.connect();

Monitoring Connection Status

Track the connection state to update your UI:
const statusDisplay = document.getElementById("status");

reactor.on("statusChanged", (status) => {
  statusDisplay.textContent = status;
  
  switch (status) {
    case "connecting":
      console.log("Establishing connection...");
      break;
    case "waiting":
      console.log("Waiting for GPU...");
      break;
    case "ready":
      console.log("Connected! Ready to interact.");
      break;
    case "disconnected":
      console.log("Disconnected");
      break;
  }
});

Showing Queue Information

Display queue position to users while waiting:
const queueDisplay = document.getElementById("queue-info");

reactor.on("waitingInfoChanged", (info) => {
  if (info.position !== undefined) {
    queueDisplay.textContent = `Position in queue: ${info.position}`;
  }
});

Sending Messages

Once connected (ready state), send messages to the GPU:
// Wait for ready state
reactor.on("statusChanged", (status) => {
  if (status === "ready") {
    // Now safe to send messages
    await reactor.sendMessage({
      action: "initialize",
      settings: { brightness: 50 }
    });
  }
});

// Or check status before sending
if (reactor.getStatus() === "ready") {
  await reactor.sendMessage({ action: "update", value: 100 });
}

Receiving Messages

Listen for messages from the GPU:
reactor.on("newMessage", (message) => {
  console.log("Received:", message);
  
  // Handle different message types
  if (message.type === "state_update") {
    updateUI(message.data);
  } else if (message.type === "notification") {
    showNotification(message.text);
  }
});

Publishing Video Input

For video-to-video models, you can publish video streams from webcams, screen captures, or any other video source:
// Capture webcam
const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    width: { ideal: 1280 },
    height: { ideal: 720 }
  }
});

// Wait for reactor to be ready
reactor.on("statusChanged", async (status) => {
  if (status === "ready") {
    // Publish the video stream
    await reactor.publishVideoStream(stream);
    console.log("Video stream published");
  }
});

// Stop publishing when needed
await reactor.unpublishVideoStream();

// Clean up the stream
stream.getTracks().forEach(track => track.stop());
Complete Video Input Example:
const videoElement = document.getElementById("localVideo") as HTMLVideoElement;
let videoStream: MediaStream | null = null;

// Start webcam
async function startWebcam() {
  try {
    videoStream = await navigator.mediaDevices.getUserMedia({
      video: { width: 1280, height: 720 }
    });
    
    // Show local preview
    videoElement.srcObject = videoStream;
    
    // Auto-publish when reactor is ready
    if (reactor.getStatus() === "ready") {
      await reactor.publishVideoStream(videoStream);
    }
  } catch (error) {
    console.error("Failed to access webcam:", error);
  }
}

// Stop webcam
async function stopWebcam() {
  if (videoStream) {
    await reactor.unpublishVideoStream();
    videoStream.getTracks().forEach(track => track.stop());
    videoElement.srcObject = null;
    videoStream = null;
  }
}

// Auto-publish when status becomes ready
reactor.on("statusChanged", async (status) => {
  if (status === "ready" && videoStream) {
    await reactor.publishVideoStream(videoStream);
  } else if (status !== "ready") {
    await reactor.unpublishVideoStream();
  }
});

// Start everything
await startWebcam();
await reactor.connect();

Cleanup

Always disconnect when done:
// Clean disconnect
await reactor.disconnect();

// Or use in cleanup handlers
window.addEventListener("beforeunload", async () => {
  await reactor.disconnect();
});

Using the React API

The React API provides a declarative way to use Reactor with built-in state management, making it ideal for React applications.

Setup

Wrap your app with ReactorProvider:
import { ReactorProvider } from "@reactor-team/js-sdk";

function App() {
  return (
    <ReactorProvider
      modelName="longlive"
      insecureApiKey={process.env.REACTOR_API_KEY}
      autoConnect={true}
    >
      <YourApp />
    </ReactorProvider>
  );
}

Displaying the Stream

Use ReactorView to show the video:
import { ReactorView } from "@reactor-team/js-sdk";

function StreamDisplay() {
  return (
    <div className="stream-container">
      <ReactorView 
        className="w-full h-96 rounded-lg"
      />
    </div>
  );
}

Accessing State

Use the useReactor hook to access state:
import { useReactor } from "@reactor-team/js-sdk";

function ConnectionStatus() {
  const status = useReactor((state) => state.status);
  
  return <div>Status: {status}</div>;
}

Connection Controls

Access connection methods:
function ConnectionControls() {
  const connect = useReactor((state) => state.connect);
  const disconnect = useReactor((state) => state.disconnect);
  const status = useReactor((state) => state.status);
  
  return (
    <div>
      {status === "disconnected" && (
        <button onClick={connect}>Connect</button>
      )}
      {status !== "disconnected" && (
        <button onClick={disconnect}>Disconnect</button>
      )}
    </div>
  );
}

Sending Messages

Send messages to the GPU:
function BrightnessControl() {
  const sendMessage = useReactor((state) => state.sendMessage);
  const status = useReactor((state) => state.status);
  const [brightness, setBrightness] = useState(50);
  
  const handleChange = async (value: number) => {
    setBrightness(value);
    if (status === "ready") {
      await sendMessage({ action: "set_brightness", value });
    }
  };
  
  return (
    <input
      type="range"
      min="0"
      max="100"
      value={brightness}
      onChange={(e) => handleChange(parseInt(e.target.value))}
      disabled={status !== "ready"}
    />
  );
}

Receiving Messages

Use useReactorMessage hook:
import { useReactorMessage } from "@reactor-team/js-sdk";

function MessageDisplay() {
  const [messages, setMessages] = useState<any[]>([]);
  
  useReactorMessage((message) => {
    setMessages((prev) => [...prev, message]);
  });
  
  return (
    <div>
      {messages.map((msg, i) => (
        <div key={i}>{JSON.stringify(msg)}</div>
      ))}
    </div>
  );
}

Publishing Video Input

For video-to-video models, use the WebcamStream component to automatically handle webcam capture and publishing:
import { WebcamStream } from "@reactor-team/js-sdk";

function VideoInput() {
  return (
    <WebcamStream 
      className="w-full aspect-video rounded-lg"
      videoConstraints={{
        width: { ideal: 1280 },
        height: { ideal: 720 }
      }}
      videoObjectFit="cover"
    />
  );
}
The WebcamStream component:
  • Automatically requests camera access on mount
  • Publishes video when Reactor status becomes "ready"
  • Unpublishes video when disconnecting
  • Shows permission errors if camera access is denied
  • Handles cleanup on unmount
Manual Control (Advanced): If you need more control over video publishing, you can use the hooks directly:
import { useReactor } from "@reactor-team/js-sdk";
import { useEffect, useRef, useState } from "react";

function CustomVideoInput() {
  const { publishVideoStream, unpublishVideoStream, status } = useReactor((state) => ({
    publishVideoStream: state.publishVideoStream,
    unpublishVideoStream: state.unpublishVideoStream,
    status: state.status
  }));
  
  const videoRef = useRef<HTMLVideoElement>(null);
  const [stream, setStream] = useState<MediaStream | null>(null);
  
  // Start webcam
  useEffect(() => {
    async function startCamera() {
      try {
        const mediaStream = await navigator.mediaDevices.getUserMedia({
          video: { width: 1280, height: 720 }
        });
        setStream(mediaStream);
        if (videoRef.current) {
          videoRef.current.srcObject = mediaStream;
        }
      } catch (error) {
        console.error("Camera error:", error);
      }
    }
    
    startCamera();
    
    return () => {
      stream?.getTracks().forEach(track => track.stop());
    };
  }, []);
  
  // Auto-publish when ready
  useEffect(() => {
    if (status === "ready" && stream) {
      publishVideoStream(stream);
    } else if (status !== "ready") {
      unpublishVideoStream();
    }
  }, [status, stream, publishVideoStream, unpublishVideoStream]);
  
  return <video ref={videoRef} autoPlay muted playsInline />;
}

Queue Display

Show queue information:
function QueueStatus() {
  const status = useReactor((state) => state.status);
  const waitingInfo = useReactor((state) => state.waitingInfo);
  
  if (status !== "waiting") return null;
  
  return (
    <div className="queue-info">
      {waitingInfo?.position && (
        <p>Position in queue: {waitingInfo.position}</p>
      )}
    </div>
  );
}

Complete React Example

Here’s a full example combining all the pieces:
import {
  ReactorProvider,
  ReactorView,
  useReactor,
  useReactorMessage
} from "@reactor-team/js-sdk";
import { useState } from "react";

export default function App() {
  return (
    <ReactorProvider
      modelName="longlive"
      insecureApiKey={process.env.REACTOR_API_KEY!}
      autoConnect={true}
    >
      <ReactorApp />
    </ReactorProvider>
  );
}

function ReactorApp() {
  const status = useReactor((state) => state.status);
  const connect = useReactor((state) => state.connect);
  const disconnect = useReactor((state) => state.disconnect);
  const sendMessage = useReactor((state) => state.sendMessage);
  const waitingInfo = useReactor((state) => state.waitingInfo);
  
  const [messages, setMessages] = useState<any[]>([]);
  
  useReactorMessage((message) => {
    setMessages((prev) => [...prev, message]);
  });
  
  return (
    <div className="container">
      <h1>Reactor Demo</h1>
      
      {/* Status Display */}
      <div className="status">
        Status: <strong>{status}</strong>
      </div>
      
      {/* Queue Info */}
      {status === "waiting" && waitingInfo && (
        <div className="queue">
          {waitingInfo.position && <p>Position: {waitingInfo.position}</p>}
        </div>
      )}
      
      {/* Video Stream */}
      <ReactorView className="video" />
      
      {/* Controls */}
      <div className="controls">
        {status === "disconnected" && (
          <button onClick={connect}>Connect</button>
        )}
        {status !== "disconnected" && (
          <button onClick={disconnect}>Disconnect</button>
        )}
        {status === "ready" && (
          <button onClick={async () => await sendMessage({ action: "ping" })}>
            Send Message
          </button>
        )}
      </div>
      
      {/* Messages */}
      <div className="messages">
        <h3>Messages</h3>
        {messages.map((msg, i) => (
          <pre key={i}>{JSON.stringify(msg, null, 2)}</pre>
        ))}
      </div>
    </div>
  );
}

Error Handling

Proper error handling ensures your application gracefully handles connection issues and provides helpful feedback to users.

Listening for Errors

Subscribe to error events:
// Imperative API
reactor.on("error", (error) => {
  console.error(`[${error.component}] ${error.message}`);
  
  if (error.recoverable) {
    console.log("Error is recoverable");
  }
});

// React API
function ErrorDisplay() {
  const lastError = useReactor((state) => state.lastError);
  
  if (!lastError) return null;
  
  return (
    <div className="error">
      <strong>{lastError.code}</strong>: {lastError.message}
    </div>
  );
}

Error Recovery

Handle recoverable errors with retry logic:
reactor.on("error", async (error) => {
  if (error.recoverable) {
    const delay = error.retryAfter || 3; // Default to 3 seconds
    console.log(`Retrying in ${delay} seconds...`);
    
    await new Promise(resolve => setTimeout(resolve, delay * 1000));
    
    try {
      await reactor.connect();
    } catch (retryError) {
      console.error("Retry failed:", retryError);
    }
  } else {
    console.error("Non-recoverable error:", error.message);
    // Show user-friendly error message
  }
});

Common Error Scenarios

Authentication Failed

reactor.on("error", (error) => {
  if (error.code === "AUTHENTICATION_FAILED") {
    alert("Invalid API key. Please check your credentials.");
    // Redirect to login or settings
  }
});

Connection Errors

reactor.on("error", (error) => {
  if (error.component === "coordinator") {
    console.log("Coordinator connection issue");
    // Show "Connecting..." state
  } else if (error.component === "gpu") {
    console.log("GPU connection issue");
    // Show "Reconnecting to GPU..." state
  }
});

Message Send Failures

reactor.on("error", (error) => {
  if (error.code === "MESSAGE_SEND_FAILED") {
    console.log("Failed to send message, will retry");
    // Queue the message for retry
  }
});

Next Steps

  • Check out the API Reference for detailed method documentation
  • Explore the Quickstart guide for a quick setup
  • Browse the Models to see what’s available
I