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: Connected to coordinator, waiting for GPU assignment
  4. ready: Connected to GPU machine, can send and receive messages

Status Flow Diagram

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

┌─────────────┐
│  connecting │
└──────┬──────┘
       │ Coordinator connection established

┌─────────────┐
│   waiting   │◄─── Waiting for GPU assignment
└──────┬──────┘
       │ GPU assigned, WebRTC established

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

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

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, fetchInsecureJwtToken } from "@reactor-team/js-sdk";

// Authenticate first
const jwtToken = await fetchInsecureJwtToken(apiKey);

// Create instance
const reactor = new Reactor({
  modelName: "livecore",
});

// Set up video display
const videoElement = document.getElementById("videoStream") as HTMLVideoElement;
reactor.on("streamChanged", (videoTrack) => {
  if (videoTrack) {
    // Create a MediaStream from the track and attach to video element
    const stream = new MediaStream([videoTrack]);
    videoElement.srcObject = stream;
    videoElement.play().catch(console.warn);
  } else {
    videoElement.srcObject = null;
  }
});

// Connect with the JWT token
await reactor.connect(jwtToken);
The status will progress through "connecting""waiting""ready" as the connection is established.

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 assignment...");
      break;
    case "ready":
      console.log("Connected! Ready to interact.");
      break;
    case "disconnected":
      console.log("Disconnected");
      break;
  }
});

Sending Commands

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

// Or check status before sending
if (reactor.getStatus() === "ready") {
  await reactor.sendCommand("set_brightness", { value: 100 });
}
The sendCommand method takes two arguments:
  • command: A string identifying the command type
  • data: An object containing the command payload

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 tracks from webcams, screen captures, or any other video source:
// Capture webcam
const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    width: { ideal: 1280 },
    height: { ideal: 720 },
  },
});

// Get the video track from the stream
const videoTrack = stream.getVideoTracks()[0];

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

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

// 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") {
      const videoTrack = videoStream.getVideoTracks()[0];
      await reactor.publishTrack(videoTrack);
    }
  } catch (error) {
    console.error("Failed to access webcam:", error);
  }
}

// Stop webcam
async function stopWebcam() {
  if (videoStream) {
    await reactor.unpublishTrack();
    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) {
    const videoTrack = videoStream.getVideoTracks()[0];
    await reactor.publishTrack(videoTrack);
  } else if (status !== "ready") {
    await reactor.unpublishTrack();
  }
});

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

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 and pass a JWT token for authentication:
import { useState, useEffect } from "react";
import { ReactorProvider, fetchInsecureJwtToken } from "@reactor-team/js-sdk";

function App() {
  const [jwtToken, setJwtToken] = useState<string | null>(null);

  useEffect(() => {
    // In production, fetch the JWT from your backend instead
    fetchInsecureJwtToken(apiKey).then(setJwtToken);
  }, []);

  if (!jwtToken) return <div>Authenticating...</div>;

  return (
    <ReactorProvider modelName="livecore" jwtToken={jwtToken} autoConnect>
      <YourApp />
    </ReactorProvider>
  );
}
See the Authentication guide for details on obtaining a JWT token. For maximum security in production, fetch the JWT token from your backend rather than using fetchInsecureJwtToken on the client.

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 Commands

Send commands to the GPU:
function BrightnessControl() {
  const sendCommand = useReactor((state) => state.sendCommand);
  const status = useReactor((state) => state.status);
  const [brightness, setBrightness] = useState(50);

  const handleChange = async (value: number) => {
    setBrightness(value);
    if (status === "ready") {
      await sendCommand("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 />;
}

Complete React Example

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

export default function App() {
  const [jwtToken, setJwtToken] = useState<string | null>(null);
  const apiKey = process.env.NEXT_PUBLIC_API_KEY!;

  useEffect(() => {
    fetchInsecureJwtToken(apiKey)
      .then(setJwtToken)
      .catch(console.error);
  }, [apiKey]);

  if (!jwtToken) return <div>Authenticating...</div>;

  return (
    <ReactorProvider modelName="livecore" jwtToken={jwtToken} autoConnect>
      <ReactorApp />
    </ReactorProvider>
  );
}

function ReactorApp() {
  const status = useReactor((state) => state.status);
  const connect = useReactor((state) => state.connect);
  const disconnect = useReactor((state) => state.disconnect);
  const sendCommand = useReactor((state) => state.sendCommand);

  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>

      {/* 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 sendCommand("ping", {})}>
            Send Command
          </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 {
      // Use reconnect() to attempt reconnection to existing session
      await reactor.reconnect();
    } 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