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
- disconnected: No active connection
- connecting: Establishing connection to the Reactor coordinator
- waiting: Connected to coordinator, waiting for GPU assignment
- 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);
}
});
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>
);
}
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