Skip to main content

React Integration

The quickrtc-react-client package provides React hooks and components for building video conferencing UIs.

Installation

npm install quickrtc-react-client socket.io-client

Quick Start

import { useState, useEffect } from "react";
import { useQuickRTC, QuickRTCVideo } from "quickrtc-react-client";
import type { LocalStream, RemoteStream } from "quickrtc-react-client";
import { io, Socket } from "socket.io-client";

function VideoRoom() {
const [socket, setSocket] = useState<Socket | null>(null);
const [localStreams, setLocalStreams] = useState<LocalStream[]>([]);
const [remoteStreams, setRemoteStreams] = useState<RemoteStream[]>([]);

const { rtc, isConnected, join, leave, produce } = useQuickRTC({
socket,
debug: true,
});

// Subscribe to events
useEffect(() => {
if (!rtc) return;

rtc.on("newParticipant", ({ participantName, streams }) => {
console.log(`${participantName} joined`);
setRemoteStreams(prev => [...prev, ...streams]);
});

rtc.on("streamAdded", (stream) => {
setRemoteStreams(prev => [...prev, stream]);
});

rtc.on("streamRemoved", ({ streamId }) => {
setRemoteStreams(prev => prev.filter(s => s.id !== streamId));
});

rtc.on("participantLeft", ({ participantId }) => {
setRemoteStreams(prev =>
prev.filter(s => s.participantId !== participantId)
);
});

rtc.on("localStreamEnded", ({ streamId }) => {
setLocalStreams(prev => prev.filter(s => s.id !== streamId));
});
}, [rtc]);

const handleJoin = async () => {
// Connect socket
const newSocket = io("https://localhost:3000");
await new Promise(resolve => newSocket.on("connect", resolve));
setSocket(newSocket);

// Join conference
await join({
conferenceId: "my-room",
participantName: "Alice",
});

// Start camera
const media = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
const streams = await produce(media.getTracks());
setLocalStreams(streams);
};

const handleLeave = async () => {
await leave();
socket?.disconnect();
setSocket(null);
setLocalStreams([]);
setRemoteStreams([]);
};

if (!isConnected) {
return <button onClick={handleJoin}>Join Room</button>;
}

return (
<div>
<button onClick={handleLeave}>Leave</button>

{/* Local video */}
{localStreams.filter(s => s.type === "video").map(stream => (
<QuickRTCVideo
key={stream.id}
stream={stream.stream}
muted
mirror
/>
))}

{/* Remote videos */}
{remoteStreams.filter(s => s.type === "video").map(stream => (
<QuickRTCVideo
key={stream.id}
stream={stream.stream}
/>
))}

{/* Remote audio (hidden) */}
{remoteStreams.filter(s => s.type === "audio").map(stream => (
<QuickRTCVideo
key={stream.id}
stream={stream.stream}
audioOnly
/>
))}
</div>
);
}

useQuickRTC Hook

Options

interface UseQuickRTCOptions {
socket: Socket | null; // Socket.IO client (can be null initially)
maxParticipants?: number; // Max participants (0 = unlimited)
debug?: boolean; // Enable debug logging
}

Return Value

interface UseQuickRTCReturn {
rtc: QuickRTC | null; // Instance for event subscriptions
isConnected: boolean; // Connection state
conferenceId: string | null; // Current conference ID
participantId: string | null; // Current participant ID

// Actions
join: (config: JoinConfig) => Promise<{ conferenceId, participantId }>;
leave: () => Promise<void>;
produce: (input: ProduceInput) => Promise<LocalStream[]>;
pause: (streamId: string) => Promise<void>;
resume: (streamId: string) => Promise<void>;
stop: (streamId: string) => Promise<void>;
}

Usage Pattern

The hook follows a state-external pattern - you manage your own stream state:

const { rtc, isConnected, join, produce } = useQuickRTC({ socket });

// Your own state
const [localStreams, setLocalStreams] = useState<LocalStream[]>([]);
const [remoteStreams, setRemoteStreams] = useState<RemoteStream[]>([]);

// Subscribe to events and update YOUR state
useEffect(() => {
if (!rtc) return;

rtc.on("newParticipant", ({ streams }) => {
setRemoteStreams(prev => [...prev, ...streams]);
});

// ... other events
}, [rtc]);

QuickRTCVideo Component

A simple video/audio element that handles MediaStream attachment.

Props

interface QuickRTCVideoProps {
stream: MediaStream; // The MediaStream to display
muted?: boolean; // Mute audio (default: false)
mirror?: boolean; // Flip horizontally (default: false)
audioOnly?: boolean; // Render as hidden audio element
className?: string; // CSS class name
style?: React.CSSProperties;
}

Examples

// Local video (muted to prevent echo, mirrored for self-view)
<QuickRTCVideo
stream={localVideoStream.stream}
muted
mirror
className="w-full rounded-lg"
/>

// Remote video
<QuickRTCVideo
stream={remoteVideoStream.stream}
className="w-full rounded-lg"
/>

// Audio-only (hidden element for remote audio)
<QuickRTCVideo
stream={remoteAudioStream.stream}
audioOnly
/>

// Screen share (no mirror, contain fit)
<QuickRTCVideo
stream={screenShareStream.stream}
style={{ objectFit: "contain" }}
/>

Events Reference

Subscribe to events via the rtc instance:

useEffect(() => {
if (!rtc) return;

const handleNewParticipant = (data) => {
console.log(data.participantName, data.streams);
};

rtc.on("newParticipant", handleNewParticipant);

return () => {
rtc.off("newParticipant", handleNewParticipant);
};
}, [rtc]);
EventDataDescription
newParticipant{ participantId, participantName, participantInfo, streams[] }Participant joined
streamAddedRemoteStreamParticipant added a stream
streamRemoved{ participantId, streamId, type }Stream removed
participantLeft{ participantId }Participant left
localStreamEnded{ streamId, type }Local track ended externally
error{ message, error }Error occurred

Complete Example

import { useState, useEffect, useCallback } from "react";
import { useQuickRTC, QuickRTCVideo } from "quickrtc-react-client";
import type { LocalStream, RemoteStream } from "quickrtc-react-client";
import { io, Socket } from "socket.io-client";

const SERVER_URL = "https://localhost:3000";

interface RemoteParticipant {
id: string;
name: string;
streams: RemoteStream[];
}

export default function App() {
// Connection state
const [name, setName] = useState("");
const [roomId, setRoomId] = useState("demo-room");
const [socket, setSocket] = useState<Socket | null>(null);

// Media state
const [localStreams, setLocalStreams] = useState<LocalStream[]>([]);
const [remoteParticipants, setRemoteParticipants] = useState<RemoteParticipant[]>([]);

const { rtc, isConnected, join, leave, produce } = useQuickRTC({
socket,
debug: true
});

// Subscribe to events
useEffect(() => {
if (!rtc) return;

const handleNewParticipant = ({ participantId, participantName, streams }) => {
setRemoteParticipants(prev => {
const exists = prev.find(p => p.id === participantId);
if (exists) return prev;
return [...prev, { id: participantId, name: participantName, streams }];
});
};

const handleStreamAdded = (stream: RemoteStream) => {
setRemoteParticipants(prev =>
prev.map(p =>
p.id === stream.participantId
? { ...p, streams: [...p.streams, stream] }
: p
)
);
};

const handleStreamRemoved = ({ participantId, streamId }) => {
setRemoteParticipants(prev =>
prev.map(p =>
p.id === participantId
? { ...p, streams: p.streams.filter(s => s.id !== streamId) }
: p
)
);
};

const handleParticipantLeft = ({ participantId }) => {
setRemoteParticipants(prev =>
prev.filter(p => p.id !== participantId)
);
};

const handleLocalStreamEnded = ({ streamId }) => {
setLocalStreams(prev => prev.filter(s => s.id !== streamId));
};

rtc.on("newParticipant", handleNewParticipant);
rtc.on("streamAdded", handleStreamAdded);
rtc.on("streamRemoved", handleStreamRemoved);
rtc.on("participantLeft", handleParticipantLeft);
rtc.on("localStreamEnded", handleLocalStreamEnded);

return () => {
rtc.off("newParticipant", handleNewParticipant);
rtc.off("streamAdded", handleStreamAdded);
rtc.off("streamRemoved", handleStreamRemoved);
rtc.off("participantLeft", handleParticipantLeft);
rtc.off("localStreamEnded", handleLocalStreamEnded);
};
}, [rtc]);

const handleJoin = useCallback(async () => {
if (!name.trim()) return;

const newSocket = io(SERVER_URL);
await new Promise<void>((resolve, reject) => {
newSocket.on("connect", () => resolve());
newSocket.on("connect_error", reject);
});
setSocket(newSocket);

await join({ conferenceId: roomId, participantName: name });
}, [name, roomId, join]);

const handleLeave = useCallback(async () => {
await leave();
socket?.disconnect();
setSocket(null);
setLocalStreams([]);
setRemoteParticipants([]);
}, [leave, socket]);

const toggleVideo = useCallback(async () => {
const hasVideo = localStreams.some(s => s.type === "video");

if (hasVideo) {
const video = localStreams.find(s => s.type === "video");
if (video) {
await video.stop();
setLocalStreams(prev => prev.filter(s => s.id !== video.id));
}
} else {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const [videoStream] = await produce(stream.getVideoTracks());
if (videoStream) {
setLocalStreams(prev => [...prev, videoStream]);
}
}
}, [localStreams, produce]);

// Join form
if (!isConnected) {
return (
<div className="min-h-screen bg-black flex items-center justify-center p-4">
<div className="bg-neutral-900 p-8 rounded-xl w-full max-w-sm">
<h1 className="text-2xl font-bold text-white mb-6">Join Room</h1>
<input
className="w-full p-3 mb-4 bg-neutral-800 text-white rounded-lg"
placeholder="Your Name"
value={name}
onChange={e => setName(e.target.value)}
/>
<input
className="w-full p-3 mb-4 bg-neutral-800 text-white rounded-lg"
placeholder="Room ID"
value={roomId}
onChange={e => setRoomId(e.target.value)}
/>
<button
className="w-full p-3 bg-white text-black rounded-lg font-semibold"
onClick={handleJoin}
>
Join
</button>
</div>
</div>
);
}

// Conference room
const localVideo = localStreams.find(s => s.type === "video");

return (
<div className="min-h-screen bg-black p-4">
<div className="flex gap-2 mb-4 justify-center">
<button
className="px-4 py-2 rounded-lg bg-neutral-800 text-white"
onClick={toggleVideo}
>
{localVideo ? "Stop Video" : "Start Video"}
</button>
<button
className="px-4 py-2 rounded-lg bg-red-600 text-white"
onClick={handleLeave}
>
Leave
</button>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Local video */}
<div className="bg-neutral-900 rounded-lg aspect-video relative">
<span className="absolute top-2 left-2 bg-black/50 text-white text-sm px-2 py-1 rounded">
You
</span>
{localVideo ? (
<QuickRTCVideo
stream={localVideo.stream}
muted
mirror
className="w-full h-full object-cover rounded-lg"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-neutral-500">
Camera off
</div>
)}
</div>

{/* Remote participants */}
{remoteParticipants.map(participant => {
const video = participant.streams.find(s => s.type === "video");
const audio = participant.streams.find(s => s.type === "audio");

return (
<div key={participant.id} className="bg-neutral-900 rounded-lg aspect-video relative">
<span className="absolute top-2 left-2 bg-black/50 text-white text-sm px-2 py-1 rounded z-10">
{participant.name}
</span>
{video ? (
<QuickRTCVideo
stream={video.stream}
className="w-full h-full object-cover rounded-lg"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-neutral-500">
{participant.name.charAt(0).toUpperCase()}
</div>
)}
{audio && <QuickRTCVideo stream={audio.stream} audioOnly />}
</div>
);
})}
</div>
</div>
);
}

Types

All types are re-exported from quickrtc-client:

import type {
LocalStream,
RemoteStream,
JoinConfig,
ProduceInput,
NewParticipantEvent,
} from "quickrtc-react-client";