
React Hooks
- 45 installs
- 3 repo stars
- Updated January 22, 2026
- codestackr/livekit-skills
Helps with frontend development tasks.
About
react-hooks is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted coding.
- react-hooks
- Frontend Development
- AI-coding skill
React Hooks by the numbers
- 45 all-time installs (skills.sh)
- Ranked #1,338 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/codestackr/livekit-skills --skill react-hooksAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 45 |
|---|---|
| repo stars | ★ 3 |
| Last updated | January 22, 2026 |
| Repository | codestackr/livekit-skills ↗ |
What it does
Helps with frontend development tasks.
Files
LiveKit React Hooks
Build custom React UIs for realtime audio/video applications with LiveKit hooks.
LiveKit MCP server tools
This skill works alongside the LiveKit MCP server, which provides direct access to the latest LiveKit documentation, code examples, and changelogs. Use these tools when you need up-to-date information that may have changed since this skill was created.
Available MCP tools:
docs_search- Search the LiveKit docs siteget_pages- Fetch specific documentation pages by pathget_changelog- Get recent releases and updates for LiveKit packagescode_search- Search LiveKit repositories for code examplesget_python_agent_example- Browse 100+ Python agent examples
When to use MCP tools:
- You need the latest API documentation or feature updates
- You're looking for recent examples or code patterns
- You want to check if a feature has been added in recent releases
- The local references don't cover a specific topic
When to use local references:
- You need quick access to core concepts covered in this skill
- You're working offline or want faster access to common patterns
- The information in the references is sufficient for your needs
Use MCP tools and local references together for the best experience.
Scope
This skill covers hooks only from @livekit/components-react. These hooks provide low-level access to LiveKit room state, participants, tracks, and agent data for building fully custom UIs.
Important: For agent applications, do NOT use UI components from `@livekit/components-react`. All UI components should come from the livekit-agents-ui skill, which provides shadcn-based components:
AgentSessionProvider- Session wrapper with audio renderingAgentControlBar- Media controlsAgentAudioVisualizerBar/Grid/Radial- Audio visualizersAgentChatTranscript- Chat display- And more
Use hooks from this skill only when you need custom behavior that the Agents UI components don't provide. The Agents UI components use these hooks internally.
References
Consult these resources as needed:
- ./references/livekit-overview.md -- LiveKit ecosystem overview and how these skills work together
- ./references/participant-hooks.md -- Hooks for accessing participant data and state
- ./references/track-hooks.md -- Hooks for working with audio/video tracks
- ./references/room-hooks.md -- Hooks for room connection and state
- ./references/session-hooks.md -- Hooks for managed agent sessions (useSession, useSessionMessages)
- ./references/agent-hooks.md -- Hooks for voice AI agent integration
- ./references/data-hooks.md -- Hooks for chat and data channels
Installation
npm install @livekit/components-react livekit-clientQuick start
Using hooks with AgentSessionProvider (standard approach)
For agent apps, use AgentSessionProvider from the livekit-agents-ui skill for the session provider. The useSession hook from this package is required to create the session for AgentSessionProvider.
Required hook: Use useSession to create the session object:
import { useRef, useEffect } from 'react';
import { useSession } from '@livekit/components-react';
import { TokenSource, TokenSourceConfigurable } from 'livekit-client';
import { AgentSessionProvider } from '@/components/agents-ui/agent-session-provider';
function App() {
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.endpoint('/api/token')
).current;
// Create session using useSession hook (required for AgentSessionProvider)
const session = useSession(tokenSource, { agentName: 'your-agent' });
useEffect(() => {
session.start();
return () => session.end();
}, []);
return (
<AgentSessionProvider session={session}>
<MyAgentUI />
</AgentSessionProvider>
);
}Additional hook for agent state: Use useVoiceAssistant to access agent state, audio tracks, and transcriptions:
import { useVoiceAssistant } from '@livekit/components-react';
// This component must be inside an AgentSessionProvider
function CustomAgentStatus() {
const { state, audioTrack, agentTranscriptions } = useVoiceAssistant();
return (
<div>
<p>Agent state: {state}</p>
{agentTranscriptions.map((t) => (
<p key={t.id}>{t.text}</p>
))}
</div>
);
}See the livekit-agents-ui skill for full component documentation.
Custom microphone toggle
import { useTrackToggle } from '@livekit/components-react';
import { Track } from 'livekit-client';
// Use this inside an AgentSessionProvider for custom toggle behavior
function CustomMicrophoneButton() {
const { enabled, toggle, pending } = useTrackToggle({
source: Track.Source.Microphone,
});
return (
<button onClick={() => toggle()} disabled={pending}>
{enabled ? 'Mute' : 'Unmute'}
</button>
);
}Fully custom approach: useSession + SessionProvider (not recommended)
Note: This pattern uses UI components from@livekit/components-reactdirectly. For agent applications, useAgentSessionProviderfrom livekit-agents-ui instead, which wraps these components and provides a better developer experience.
For fully custom implementations without Agents UI components, you can use useSession with SessionProvider and RoomAudioRenderer directly. This gives you complete control but requires more manual setup.
Use this pattern only when you cannot use AgentSessionProvider from Agents UI:
import { useEffect, useRef } from 'react';
import { useSession, useAgent, SessionProvider, RoomAudioRenderer } from '@livekit/components-react';
import { TokenSource, TokenSourceConfigurable } from 'livekit-client';
function AgentApp() {
// Use useRef to prevent recreating TokenSource on each render
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.sandboxTokenServer('your-sandbox-id')
).current;
const session = useSession(tokenSource, {
agentName: 'your-agent-name',
});
const agent = useAgent(session);
// Auto-start session with cleanup
useEffect(() => {
session.start();
return () => {
session.end();
};
}, []);
return (
<SessionProvider session={session}>
<RoomAudioRenderer />
<div>
<p>Connection: {session.connectionState}</p>
<p>Agent: {agent.state}</p>
</div>
</SessionProvider>
);
}For production, use TokenSource.endpoint() instead of the sandbox:
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.endpoint('/api/token')
).current;
const session = useSession(tokenSource, {
roomName: 'my-room',
participantIdentity: 'user-123',
participantName: 'John',
agentName: 'my-agent',
});Hook categories
Participant hooks
Access participant data and state:
useParticipants()- All participants (local + remote)useLocalParticipant()- Local participant with media stateuseRemoteParticipants()- All remote participantsuseRemoteParticipant(identity)- Specific remote participantuseParticipantInfo()- Identity, name, metadatauseParticipantAttributes()- Participant attributes
Track hooks
Work with audio/video tracks:
useTracks(sources)- Array of track referencesuseParticipantTracks(sources, identity)- Tracks for specific participantuseTrackToggle({ source })- Toggle mic/camera/screenuseIsMuted(trackRef)- Check if track is muteduseIsSpeaking(participant)- Check if participant is speakinguseTrackVolume(track)- Audio volume level
Room hooks
Room connection and state:
useConnectionState()- Room connection stateuseRoomInfo()- Room name and metadatauseLiveKitRoom(props)- Create and manage room instanceuseIsRecording()- Check if room is being recordeduseMediaDeviceSelect({ kind })- Select audio/video devices
Session hooks (beta)
For session management (required for AgentSessionProvider):
useSession(tokenSource, options)- Create and manage agent session with connection lifecycle. Required forAgentSessionProvider.useSessionMessages(session)- Combined chat and transcription messages
Agent hooks (beta)
Voice AI agent integration:
useVoiceAssistant()- Primary hook for agent state, tracks, and transcriptions. Works insideAgentSessionProvider.useAgent(session)- Full agent state with lifecycle helpers. Requires session fromuseSession.
Data hooks
Chat and data channels:
useChat()- Send/receive chat messagesuseDataChannel(topic)- Low-level data messaginguseTextStream(topic)- Subscribe to text streams (beta)useTranscriptions()- Get transcription data (beta)useEvents(instance, event, handler)- Subscribe to typed events from session/agent
Context requirement
Most hooks require a room context. For agent applications, there are two approaches:
Option 1: useSession + AgentSessionProvider (standard)
Use useSession to create a session, then pass it to AgentSessionProvider from livekit-agents-ui. The AgentSessionProvider wraps SessionProvider and includes RoomAudioRenderer for audio playback. Hooks like useVoiceAssistant, useTrackToggle, useChat, and others work automatically inside this provider.
import { useRef, useEffect } from 'react';
import { useSession, useVoiceAssistant } from '@livekit/components-react';
import { TokenSource, TokenSourceConfigurable } from 'livekit-client';
import { AgentSessionProvider } from '@/components/agents-ui/agent-session-provider';
function App() {
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.endpoint('/api/token')
).current;
// Create session using useSession hook (required)
const session = useSession(tokenSource, { agentName: 'your-agent' });
useEffect(() => {
session.start();
return () => session.end();
}, []);
return (
<AgentSessionProvider session={session}>
{/* Hooks from @livekit/components-react work here */}
<MyAgentComponent />
</AgentSessionProvider>
);
}
function MyAgentComponent() {
// useVoiceAssistant works inside AgentSessionProvider
const { state, audioTrack } = useVoiceAssistant();
return <div>Agent: {state}</div>;
}Option 2: useSession + SessionProvider (not recommended)
Note: This pattern uses UI components from@livekit/components-reactdirectly. For agent applications, use Option 1 withAgentSessionProviderfrom livekit-agents-ui instead.
Only use this pattern if you need full manual control without using Agents UI components. You must include RoomAudioRenderer manually.
import { useRef, useEffect } from 'react';
import { useSession, useAgent, SessionProvider, RoomAudioRenderer } from '@livekit/components-react';
import { TokenSource, TokenSourceConfigurable } from 'livekit-client';
function App() {
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.sandboxTokenServer('your-sandbox-id')
).current;
const session = useSession(tokenSource, { agentName: 'your-agent' });
const agent = useAgent(session); // Pass session explicitly when using useSession
useEffect(() => {
session.start();
return () => session.end();
}, []);
return (
<SessionProvider session={session}>
<RoomAudioRenderer />
<MyAgentComponent agent={agent} />
</SessionProvider>
);
}Best practices
General
1. Use Agents UI for standard UIs - For most agent applications, use the pre-built components from livekit-agents-ui. Use these hooks only when you need custom behavior. 2. Optimize with updateOnlyOn - Many hooks accept updateOnlyOn to limit re-renders to specific events. 3. Handle connection states - Always check useConnectionState() before accessing room data. 4. Memoize TokenSource - Always use useRef when creating a TokenSource to prevent recreation on each render.
For agent applications
5. Use useSession with AgentSessionProvider - For most agent apps, create a session with useSession and pass it to AgentSessionProvider from livekit-agents-ui. The AgentSessionProvider handles audio rendering automatically.
6. Use useVoiceAssistant for agent state - Inside AgentSessionProvider, use useVoiceAssistant to access agent state and transcriptions. This is simpler than useAgent.
import { useVoiceAssistant } from '@livekit/components-react';
function AgentDisplay() {
const { state, audioTrack, agentTranscriptions } = useVoiceAssistant();
// state: "disconnected" | "connecting" | "initializing" | "listening" | "thinking" | "speaking"
}7. Handle agent states properly - When using useAgent, handle all states including 'idle', 'pre-connect-buffering', and 'failed':
const agent = useAgent(session);
if (agent.state === 'failed') {
console.error('Agent failed:', agent.failureReasons);
}
if (agent.isPending) {
// Show loading state
}8. Always use AgentSessionProvider - Use useSession + AgentSessionProvider from livekit-agents-ui for all agent applications. This is the standard and recommended approach.
Performance
9. Use LiveKit's built-in hooks for media controls - For track toggling, device selection, and similar features, use the provided hooks (useTrackToggle, useMediaDeviceSelect) rather than implementing your own. These hooks handle complex state management and have been rigorously tested.
10. Subscribe to events with useEvents - Instead of manually managing event listeners, use useEvents to subscribe to session and agent events with proper cleanup:
useEvents(agent, AgentEvent.StateChanged, (state) => {
console.log('Agent state:', state);
});Beta hooks
Several hooks in @livekit/components-react are marked as beta and may change:
useSession,useSessionMessagesuseAgent,useVoiceAssistantuseTextStream,useTranscriptions
Check the LiveKit components changelog for updates to these hooks.
Agent hooks
Hooks for integrating with LiveKit voice AI agents. These hooks are marked as beta and may change.
For pre-built agent UI components, use the livekit-agents-ui skill instead, which provides AgentAudioVisualizerBar, AgentControlBar, AgentChatTranscript, and other ready-to-use components.
Choosing between useVoiceAssistant and useAgent
| Hook | Use with | When to use |
|---|---|---|
useVoiceAssistant | useSession + AgentSessionProvider | Most apps. Simple access to agent state, tracks, and transcriptions. |
useAgent | useSession + SessionProvider (or AgentSessionProvider) | Advanced apps needing full lifecycle control with waitUntil* methods. |
Both hooks work inside AgentSessionProvider. Use useVoiceAssistant for simpler access to agent state, or useAgent when you need lifecycle methods like waitUntilConnected().
useVoiceAssistant (beta)
Get the state, tracks, and transcriptions of a voice assistant agent. This is the primary hook for agent state when using AgentSessionProvider.
This hook works inside AgentSessionProvider, which requires a session from useSession:
import { useRef, useEffect } from 'react';
import { useSession, useVoiceAssistant } from '@livekit/components-react';
import { TokenSource, TokenSourceConfigurable } from 'livekit-client';
import { AgentSessionProvider } from '@/components/agents-ui/agent-session-provider';
function App() {
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.endpoint('/api/token')
).current;
// Create session using useSession hook (required for AgentSessionProvider)
const session = useSession(tokenSource, { agentName: 'your-agent' });
useEffect(() => {
session.start();
return () => session.end();
}, []);
return (
<AgentSessionProvider session={session}>
<VoiceAssistantUI />
</AgentSessionProvider>
);
}
function VoiceAssistantUI() {
// Works inside AgentSessionProvider
const {
agent,
state,
audioTrack,
videoTrack,
agentTranscriptions,
agentAttributes,
} = useVoiceAssistant();
return (
<div>
<p>State: {state}</p>
{agentTranscriptions.map((segment) => (
<p key={segment.id}>{segment.text}</p>
))}
</div>
);
}Return values
| Property | Type | Description |
|---|---|---|
agent | `RemoteParticipant \ | undefined` |
state | AgentState | Current agent state |
audioTrack | `TrackReference \ | undefined` |
videoTrack | `TrackReference \ | undefined` |
agentTranscriptions | ReceivedTranscriptionSegment[] | Agent speech transcriptions |
agentAttributes | `Participant['attributes'] \ | undefined` |
Agent states
The useVoiceAssistant hook returns a simplified subset of agent states:
type AgentState =
| 'disconnected' // Room not connected
| 'connecting' // Waiting for agent to join
| 'initializing' // Agent joined, setting up
| 'listening' // Agent listening for user input
| 'thinking' // Agent processing/generating response
| 'speaking'; // Agent speakingFor the full set of states including 'idle', 'pre-connect-buffering', and 'failed', use useAgent instead.
State-based UI
function AgentStatusIndicator() {
const { state } = useVoiceAssistant();
const stateColors = {
disconnected: 'bg-gray-500',
connecting: 'bg-yellow-500 animate-pulse',
initializing: 'bg-yellow-500 animate-pulse',
listening: 'bg-blue-500',
thinking: 'bg-purple-500 animate-pulse',
speaking: 'bg-green-500',
};
return (
<div className={`w-3 h-3 rounded-full ${stateColors[state]}`} />
);
}Audio visualization
For audio visualization UI, use AgentAudioVisualizerBar, AgentAudioVisualizerGrid, or AgentAudioVisualizerRadial from livekit-agents-ui. These components handle audio track subscription and visualization automatically.
If you need raw audio volume data for a custom implementation, use the useMultibandTrackVolume or useTrackVolume hooks from the track-hooks reference.
Requirements
This hook requires an agent running with livekit-agents >= 0.9.0.
useAgent (beta)
Full agent state management with lifecycle helpers. This hook provides lifecycle methods like waitUntilConnected() that are not available in useVoiceAssistant.
Requires `useSession`: Pass the session from useSession explicitly to this hook. Works with both AgentSessionProvider and SessionProvider.
import { useRef, useEffect } from 'react';
import { useSession, useAgent } from '@livekit/components-react';
import { TokenSource, TokenSourceConfigurable } from 'livekit-client';
function App() {
// Use useRef to prevent recreating TokenSource on each render
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.sandboxTokenServer('your-sandbox-id')
).current;
const session = useSession(tokenSource, { agentName: 'your-agent' });
const agent = useAgent(session); // Pass session explicitly
useEffect(() => {
session.start();
return () => session.end();
}, []);
if (agent.state === 'connecting') {
return <div>Waiting for agent...</div>;
}
if (agent.state === 'failed') {
return <div>Failed: {agent.failureReasons?.join(', ')}</div>;
}
return (
<div>
<p>Agent: {agent.name}</p>
<p>State: {agent.state}</p>
<p>Connected: {agent.isConnected ? 'Yes' : 'No'}</p>
</div>
);
}Return values
| Property | Type | Description |
|---|---|---|
state | AgentState | Current agent state |
identity | `string \ | undefined` |
name | `string \ | undefined` |
metadata | `string \ | undefined` |
attributes | Participant['attributes'] | Agent's attributes |
isConnected | boolean | Whether agent is connected and ready |
canListen | boolean | Whether client could be listening (includes pre-connect buffering) |
isFinished | boolean | Whether session has ended |
isPending | boolean | Whether agent is connecting/initializing |
failureReasons | `string[] \ | null` |
cameraTrack | `TrackReference \ | undefined` |
microphoneTrack | `TrackReference \ | undefined` |
Lifecycle methods
| Method | Description |
|---|---|
waitUntilConnected(signal?) | Promise that resolves when agent is connected |
waitUntilCouldBeListening(signal?) | Promise that resolves when client could be listening |
waitUntilFinished(signal?) | Promise that resolves when session ends |
waitUntilCamera(signal?) | Promise that resolves when camera track is available |
waitUntilMicrophone(signal?) | Promise that resolves when microphone track is available |
Agent state lifecycle
For agents with pre-connect audio buffer enabled:
connecting -> pre-connect-buffering -> initializing/listening/thinking/speakingFor agents without pre-connect audio:
connecting -> initializing -> idle/listening/thinking/speakingOn failure:
connecting -> pre-connect-buffering/initializing -> failedExtended agent states
type AgentState =
| 'disconnected' // Room not connected
| 'connecting' // Waiting for agent
| 'pre-connect-buffering' // Recording audio before agent connects
| 'failed' // Agent failed to connect
| 'initializing' // Agent setting up
| 'idle' // Agent idle
| 'listening' // Listening for input
| 'thinking' // Processing
| 'speaking'; // SpeakingState-based UI with all states
These examples assume you're using the useSession + SessionProvider pattern (see example above) and passing agent as a prop or using context:
// Inside a SessionProvider with useAgent(session) from parent
function AgentStatusIndicator({ agent }: { agent: UseAgentReturn }) {
const stateColors: Record<AgentState, string> = {
disconnected: 'bg-gray-500',
connecting: 'bg-yellow-500 animate-pulse',
'pre-connect-buffering': 'bg-yellow-500 animate-pulse',
failed: 'bg-red-500',
initializing: 'bg-yellow-500 animate-pulse',
idle: 'bg-gray-400',
listening: 'bg-blue-500',
thinking: 'bg-purple-500 animate-pulse',
speaking: 'bg-green-500',
};
return (
<div className={`w-3 h-3 rounded-full ${stateColors[agent.state]}`} />
);
}Waiting for agent connection
function WaitForAgent({ agent }: { agent: UseAgentReturn }) {
const [ready, setReady] = useState(false);
useEffect(() => {
const controller = new AbortController();
agent.waitUntilConnected(controller.signal)
.then(() => setReady(true))
.catch((e) => console.log('Cancelled or failed:', e));
return () => controller.abort();
}, [agent]);
if (!ready) {
return <LoadingSpinner />;
}
return <AgentUI agent={agent} />;
}Error handling
function AgentWithErrorHandling({ agent }: { agent: UseAgentReturn }) {
if (agent.state === 'failed') {
return (
<div className="error">
<h2>Agent Connection Failed</h2>
<ul>
{agent.failureReasons?.map((reason, i) => (
<li key={i}>{reason}</li>
))}
</ul>
<button onClick={() => window.location.reload()}>
Try Again
</button>
</div>
);
}
return <AgentUI />;
}Usage with useSession
The useAgent hook requires a session from useSession. Use it with AgentSessionProvider from livekit-agents-ui:
With AgentSessionProvider (standard):
import { useRef, useEffect } from 'react';
import { useSession, useAgent } from '@livekit/components-react';
import { TokenSource, TokenSourceConfigurable } from 'livekit-client';
import { AgentSessionProvider } from '@/components/agents-ui/agent-session-provider';
function AgentApp() {
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.endpoint('/api/token')
).current;
const session = useSession(tokenSource, { agentName: 'my-agent' });
const agent = useAgent(session);
useEffect(() => {
session.start();
return () => session.end();
}, []);
return (
<AgentSessionProvider session={session}>
{/* Use agent.state, agent.waitUntilConnected(), etc. */}
</AgentSessionProvider>
);
}With SessionProvider (not recommended):
Note: This pattern uses UI components from@livekit/components-reactdirectly. For agent applications, useAgentSessionProviderfrom livekit-agents-ui instead.
import { useRef, useEffect } from 'react';
import { useSession, useAgent, SessionProvider, RoomAudioRenderer } from '@livekit/components-react';
import { TokenSource, TokenSourceConfigurable } from 'livekit-client';
function CustomAgentApp() {
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.endpoint('/api/token')
).current;
const session = useSession(tokenSource, {
roomName: 'my-room',
participantIdentity: 'user-123',
agentName: 'my-agent',
});
const agent = useAgent(session);
useEffect(() => {
session.start();
return () => session.end();
}, []);
return (
<SessionProvider session={session}>
<RoomAudioRenderer />
{/* Use agent.state, agent.microphoneTrack, etc. */}
</SessionProvider>
);
}For most apps, use AgentSessionProvider with useVoiceAssistant for simpler agent state access. Use useAgent when you need lifecycle methods like waitUntilConnected(). See the livekit-agents-ui skill.
Data hooks
Hooks for chat, data channels, text streams, and transcriptions.
For pre-built chat UI, use AgentChatTranscript from livekit-agents-ui. Use these hooks when building custom chat implementations.
useChat
Send and receive chat messages in a LiveKit room.
import { useChat } from '@livekit/components-react';
function ChatBox() {
const { chatMessages, send, isSending } = useChat();
const [message, setMessage] = useState('');
const handleSend = () => {
if (message.trim()) {
send(message);
setMessage('');
}
};
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto">
{chatMessages.map((msg) => (
<div key={msg.timestamp} className="p-2">
<span className="font-bold">{msg.from?.name}: </span>
<span>{msg.message}</span>
</div>
))}
</div>
<div className="flex gap-2 p-2">
<input
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
placeholder="Type a message..."
className="flex-1 px-3 py-2 border rounded"
/>
<button onClick={handleSend} disabled={isSending}>
Send
</button>
</div>
</div>
);
}Return values
| Property | Type | Description |
|---|---|---|
chatMessages | ReceivedChatMessage[] | Array of received messages |
send | (message: string) => Promise<void> | Function to send a message |
isSending | boolean | Whether a message is being sent |
Message type
interface ReceivedChatMessage {
id: string;
timestamp: number;
message: string;
from?: Participant;
}Chat options
const { chatMessages, send } = useChat({
room: customRoom, // Optional: use specific room
});Message history
Message history is not persisted by default. Messages are lost on page refresh. To persist messages, store them in your own state management or database.
useDataChannel
Low-level data channel messaging for custom data types.
import { useDataChannel } from '@livekit/components-react';
function CustomData() {
const { message, send, isSending } = useDataChannel('custom-topic', (msg) => {
console.log('Received:', msg);
});
const sendData = () => {
const data = new TextEncoder().encode(JSON.stringify({ type: 'ping' }));
send(data, { reliable: true });
};
return (
<div>
<button onClick={sendData} disabled={isSending}>
Send Ping
</button>
{message && <p>Last message from: {message.from?.identity}</p>}
</div>
);
}With topic filtering
// Only receive messages with topic 'game-state'
const { message, send } = useDataChannel('game-state', (msg) => {
const gameState = JSON.parse(new TextDecoder().decode(msg.payload));
updateGame(gameState);
});Without topic (receive all messages)
const { message, send } = useDataChannel((msg) => {
console.log('Received message on topic:', msg.topic);
});Return values
| Property | Type | Description |
|---|---|---|
message | `ReceivedDataMessage \ | undefined` |
send | (payload: Uint8Array, options: DataPublishOptions) => Promise<void> | Send data |
isSending | boolean | Whether data is being sent |
DataPublishOptions
interface DataPublishOptions {
reliable?: boolean; // Use reliable transport (default: true)
destinationIdentities?: string[]; // Send to specific participants
topic?: string; // Message topic
}useTextStream (beta)
Subscribe to text streams from a specific topic.
import { useTextStream } from '@livekit/components-react';
function TextStreamDisplay() {
const { textStreams } = useTextStream('llm-output');
return (
<div>
{textStreams.map((stream) => (
<div key={stream.streamInfo.id}>
<span className="text-gray-500">
{stream.participantInfo.identity}:
</span>
<span>{stream.text}</span>
</div>
))}
</div>
);
}Return values
| Property | Type | Description |
|---|---|---|
textStreams | TextStreamData[] | Array of text stream data |
TextStreamData type
interface TextStreamData {
text: string;
participantInfo: {
identity: string;
name?: string;
};
streamInfo: {
id: string;
timestamp: number;
};
}Options
| Option | Type | Description |
|---|---|---|
room | Room | Use a specific room instead of context |
useTranscriptions (beta)
Get transcription data from the room. Uses the lk.transcription topic internally with useTextStream. Returns TextStreamData[] (the same type as useTextStream).
import { useTranscriptions } from '@livekit/components-react';
function TranscriptDisplay() {
const transcriptions = useTranscriptions();
return (
<div className="space-y-2">
{transcriptions.map((t) => (
<div key={t.streamInfo.id} className="p-2 bg-gray-100 rounded">
<span className="font-medium">
{t.participantInfo.identity}:
</span>
<span className="ml-2">{t.text}</span>
</div>
))}
</div>
);
}Filter by participant
const transcriptions = useTranscriptions({
participantIdentities: ['agent-1', 'user-1'],
});Filter by track
const transcriptions = useTranscriptions({
trackSids: ['TR_microphone_abc123'],
});Options
| Option | Type | Description |
|---|---|---|
participantIdentities | string[] | Filter by participant identities |
trackSids | string[] | Filter by track SIDs |
room | Room | Use a specific room instead of context |
Live transcription display
function LiveTranscription() {
const transcriptions = useTranscriptions();
const containerRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom
useEffect(() => {
if (containerRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight;
}
}, [transcriptions]);
return (
<div ref={containerRef} className="h-64 overflow-y-auto">
{transcriptions.map((t, i) => (
<p key={i} className="text-sm">
<span className="text-gray-500">{t.participantInfo.identity}:</span>
{' '}{t.text}
</p>
))}
</div>
);
}useSessionMessages (beta)
Combined hook for getting all session messages (transcriptions + chat) sorted by time.
Context requirement: This hook requires a session from useSession, passed explicitly.
import { useSession, useSessionMessages, UseSessionReturn } from '@livekit/components-react';
import { TokenSource } from 'livekit-client';
function App() {
const tokenSource = TokenSource.literal({ serverUrl, participantToken: token });
const session = useSession(tokenSource);
return <SessionTranscript session={session} />;
}
function SessionTranscript({ session }: { session: UseSessionReturn }) {
const { messages, send, isSending } = useSessionMessages(session);
return (
<div>
{messages.map((msg) => (
<div key={msg.id}>
{msg.type === 'userTranscript' && (
<p className="text-blue-600">You: {msg.message}</p>
)}
{msg.type === 'agentTranscript' && (
<p className="text-green-600">Agent: {msg.message}</p>
)}
{msg.type === 'chat' && (
<p className="text-gray-600">{msg.from?.name}: {msg.message}</p>
)}
</div>
))}
</div>
);
}Return values
| Property | Type | Description |
|---|---|---|
messages | ReceivedMessage[] | All messages sorted by time |
send | (message: string, options?: SendTextOptions) => Promise<ReceivedChatMessage> | Send a chat message |
isSending | boolean | Whether a message is being sent |
Message types
type ReceivedMessage =
| ReceivedChatMessage
| ReceivedUserTranscriptionMessage
| ReceivedAgentTranscriptionMessage;
interface ReceivedUserTranscriptionMessage {
type: 'userTranscript';
id: string;
timestamp: number;
message: string;
from: LocalParticipant;
}
interface ReceivedAgentTranscriptionMessage {
type: 'agentTranscript';
id: string;
timestamp: number;
message: string;
from: RemoteParticipant;
}Usage with useSession
import { useSession, useSessionMessages } from '@livekit/components-react';
import { TokenSource } from 'livekit-client';
function SessionChat() {
const tokenSource = TokenSource.literal({ serverUrl, participantToken: token });
const session = useSession(tokenSource);
const { messages } = useSessionMessages(session);
// ...
}useEvents
Subscribe to typed events from a session, agent, or any typed event emitter. This is a utility hook for handling events from other hooks.
import { useSession, useAgent, useEvents, SessionEvent, AgentEvent } from '@livekit/components-react';
import { TokenSource } from 'livekit-client';
function EventListeners() {
const tokenSource = TokenSource.literal({ serverUrl, participantToken: token });
const session = useSession(tokenSource);
const agent = useAgent(session);
// Listen for session connection state changes
useEvents(session, SessionEvent.ConnectionStateChanged, (newState) => {
console.log('Session state changed:', newState);
});
// Listen for agent state changes
useEvents(agent, AgentEvent.StateChanged, (newState) => {
console.log('Agent state changed:', newState);
});
// Listen for agent microphone track changes
useEvents(agent, AgentEvent.MicrophoneChanged, (track) => {
console.log('Agent microphone track:', track);
});
return <div>Listening for events...</div>;
}Parameters
| Parameter | Type | Description |
|---|---|---|
instance | `Emitter \ | { internal: { emitter: Emitter } }` |
event | string | The event name to listen for |
handlerFn | Function | Callback function when event fires |
dependencies | DependencyList | Optional React dependency array for the handler |
Session events
enum SessionEvent {
ConnectionStateChanged = 'connectionStateChanged',
MediaDevicesError = 'mediaDevicesError',
EncryptionError = 'encryptionError',
}Agent events
enum AgentEvent {
CameraChanged = 'cameraChanged',
MicrophoneChanged = 'microphoneChanged',
StateChanged = 'stateChanged',
}Messages events
enum MessagesEvent {
MessageReceived = 'messageReceived',
}Usage with dependencies
import { TokenSource } from 'livekit-client';
function EventHandler({ userId }: { userId: string }) {
const tokenSource = TokenSource.literal({ serverUrl, participantToken: token });
const session = useSession(tokenSource);
// Handler will be recreated when userId changes
useEvents(
session,
SessionEvent.ConnectionStateChanged,
(state) => {
console.log(`User ${userId} connection state:`, state);
},
[userId]
);
return null;
}LiveKit overview
LiveKit is a realtime communication platform for building AI-native applications with audio, video, and data streaming. This overview helps you understand the LiveKit ecosystem and how to use these skills effectively.
Platform components
LiveKit Cloud
LiveKit Cloud is a fully managed platform for building, deploying, and operating AI agent applications. It includes:
- Realtime media infrastructure - Global mesh of servers for low-latency audio, video, and data streaming
- Managed agent hosting - Deploy agents without managing servers or orchestration
- LiveKit Inference - Run AI models directly within LiveKit Cloud without API keys
- Native telephony - Provision phone numbers and connect PSTN calls directly to rooms
- Observability - Built-in analytics, logs, and quality metrics
Agents framework
The Agents framework lets you build Python or Node.js programs that join LiveKit rooms as realtime participants. Key capabilities:
- Voice pipelines - Stream audio through STT-LLM-TTS pipelines
- Realtime models - Use models like OpenAI Realtime API that handle speech directly
- Tool calling - Define functions the LLM can invoke during conversations
- Multi-agent workflows - Hand off between specialized agents
- Turn detection - State-of-the-art model for natural conversation flow
Architecture
┌─────────────┐ WebRTC ┌─────────────┐ HTTP/WS ┌─────────────┐
│ Frontend │ ◄─────────────► │ LiveKit │ ◄──────────────► │ Agent │
│ (Web/App) │ │ Room │ │ Server │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Telephony │ │ AI Models │
│ (SIP) │ │ (STT/LLM/TTS)│
└─────────────┘ └─────────────┘How these skills work together
The LiveKit skills cover the full stack for building voice AI applications:
| Skill | Purpose | Language |
|---|---|---|
agents-py | Build agent backends | Python |
agents-ts | Build agent backends | TypeScript/Node.js |
agents-ui | Build agent frontends with shadcn components | React |
react-hooks | Build custom UIs with LiveKit hooks | React |
Typical workflow:
1. Choose your backend - Use agents-py or agents-ts based on your team's preference 2. Build the frontend - Use agents-ui for pre-built shadcn components, or react-hooks for custom UIs 3. Connect via LiveKit - Both connect to the same LiveKit room for realtime communication
Using the skills effectively
When to use each skill
- Building a new voice agent? Start with
agents-pyoragents-tsfor the backend logic - Need pre-built UI components? Use
agents-uifor shadcn-based React components - Need custom UI components? Use
react-hooksto build your own components with LiveKit hooks - Full-stack project? Use both a backend skill and a frontend skill together
Combining skills
The skills are designed to work together. A typical project structure:
my-voice-app/
├── agent/ # Use agents-py or agents-ts skill
│ └── agent.py # or agent.ts
├── frontend/ # Use agents-ui or react-hooks skill
│ └── src/
│ └── app/
└── .env.local # Shared LiveKit credentialsEnvironment setup
All skills require LiveKit credentials:
LIVEKIT_API_KEY=your_api_key
LIVEKIT_API_SECRET=your_api_secret
LIVEKIT_URL=wss://your-project.livekit.cloudGet these from your LiveKit Cloud dashboard or self-hosted deployment.
Resources
Participant hooks
Hooks for accessing participant data and state in a LiveKit room.
useParticipants
Returns all participants (local and remote) in the current room.
import { useParticipants } from '@livekit/components-react';
function ParticipantList() {
const participants = useParticipants();
return (
<ul>
{participants.map((p) => (
<li key={p.identity}>{p.name || p.identity}</li>
))}
</ul>
);
}Options
| Option | Type | Description |
|---|---|---|
updateOnlyOn | RoomEvent[] | Limit re-renders to specific room events |
room | Room | Use a specific room instead of context |
Performance optimization
import { RoomEvent } from 'livekit-client';
// Only update when participants connect/disconnect
const participants = useParticipants({
updateOnlyOn: [
RoomEvent.ParticipantConnected,
RoomEvent.ParticipantDisconnected,
],
});useLocalParticipant
Returns the local participant with media state information.
import { useLocalParticipant } from '@livekit/components-react';
function LocalStatus() {
const {
localParticipant,
isMicrophoneEnabled,
isCameraEnabled,
isScreenShareEnabled,
microphoneTrack,
cameraTrack,
lastMicrophoneError,
lastCameraError,
} = useLocalParticipant();
return (
<div>
<p>Identity: {localParticipant.identity}</p>
<p>Mic: {isMicrophoneEnabled ? 'On' : 'Off'}</p>
<p>Camera: {isCameraEnabled ? 'On' : 'Off'}</p>
{lastMicrophoneError && <p>Mic error: {lastMicrophoneError.message}</p>}
</div>
);
}Return values
| Property | Type | Description |
|---|---|---|
localParticipant | LocalParticipant | The local participant object |
isMicrophoneEnabled | boolean | Whether microphone is enabled |
isCameraEnabled | boolean | Whether camera is enabled |
isScreenShareEnabled | boolean | Whether screen share is enabled |
microphoneTrack | `TrackPublication \ | undefined` |
cameraTrack | `TrackPublication \ | undefined` |
lastMicrophoneError | `Error \ | undefined` |
lastCameraError | `Error \ | undefined` |
useRemoteParticipants
Returns all remote participants (without the local participant).
import { useRemoteParticipants } from '@livekit/components-react';
function RemoteList() {
const remoteParticipants = useRemoteParticipants();
return (
<div>
<p>{remoteParticipants.length} remote participants</p>
{remoteParticipants.map((p) => (
<div key={p.identity}>{p.name}</div>
))}
</div>
);
}Options
| Option | Type | Description |
|---|---|---|
updateOnlyOn | RoomEvent[] | Limit re-renders to specific room events |
room | Room | Use a specific room instead of context |
useRemoteParticipant
Returns a specific remote participant by identity or by participant kind.
import { useRemoteParticipant } from '@livekit/components-react';
import { ParticipantKind } from 'livekit-client';
// By identity
function SpecificParticipant({ identity }: { identity: string }) {
const participant = useRemoteParticipant(identity);
if (!participant) {
return <div>Participant not found</div>;
}
return <div>{participant.name}</div>;
}
// By kind (e.g., find the agent)
function AgentParticipant() {
const agent = useRemoteParticipant({ kind: ParticipantKind.AGENT });
if (!agent) {
return <div>Agent not connected</div>;
}
return <div>Agent: {agent.name}</div>;
}Overloads
// Find by identity string
useRemoteParticipant(identity: string, options?: UseRemoteParticipantOptions)
// Find by identifier (kind, identity, or both)
useRemoteParticipant(identifier: ParticipantIdentifier, options?: UseRemoteParticipantOptions)Options
| Option | Type | Description |
|---|---|---|
updateOnlyOn | ParticipantEvent[] | Limit re-renders to specific participant events |
useSortedParticipants
Returns participants sorted by importance (speaking, video enabled, etc.).
import { useSortedParticipants } from '@livekit/components-react';
function SortedList() {
const participants = useParticipants();
const sortedParticipants = useSortedParticipants(participants);
return (
<div>
{sortedParticipants.map((p, index) => (
<div key={p.identity}>
{index + 1}. {p.name}
</div>
))}
</div>
);
}useParticipantInfo
Returns the identity, name, and metadata of a participant.
import { useParticipantInfo } from '@livekit/components-react';
function ParticipantCard({ participant }: { participant: Participant }) {
const { identity, name, metadata } = useParticipantInfo({ participant });
return (
<div>
<h3>{name || identity}</h3>
{metadata && <p>Metadata: {metadata}</p>}
</div>
);
}Usage with context
When used inside a ParticipantContext, the participant prop is optional:
import { ParticipantContext, useParticipantInfo } from '@livekit/components-react';
function ParticipantName() {
// Uses participant from context
const { name, identity } = useParticipantInfo();
return <span>{name || identity}</span>;
}useParticipantAttributes
Returns the attributes of a participant.
import { useParticipantAttributes } from '@livekit/components-react';
function ParticipantRole({ participant }: { participant: Participant }) {
const { attributes } = useParticipantAttributes({ participant });
return (
<div>
{attributes?.role && <span>Role: {attributes.role}</span>}
</div>
);
}Options
| Option | Type | Description |
|---|---|---|
participant | Participant | The participant to get attributes from |
useLocalParticipantPermissions
Returns the local participant's permissions.
import { useLocalParticipantPermissions } from '@livekit/components-react';
function PermissionStatus() {
const permissions = useLocalParticipantPermissions();
if (!permissions) {
return <div>Loading permissions...</div>;
}
return (
<div>
<p>Can publish: {permissions.canPublish ? 'Yes' : 'No'}</p>
<p>Can subscribe: {permissions.canSubscribe ? 'Yes' : 'No'}</p>
<p>Can publish data: {permissions.canPublishData ? 'Yes' : 'No'}</p>
</div>
);
}Return type
Returns ParticipantPermission | undefined with properties:
| Property | Type | Description |
|---|---|---|
canPublish | boolean | Can publish tracks |
canSubscribe | boolean | Can subscribe to tracks |
canPublishData | boolean | Can publish data messages |
canPublishSources | TrackSource[] | Specific sources allowed to publish |
canUpdateMetadata | boolean | Can update own metadata |
Room hooks
Hooks for room connection, state, and media device management.
useConnectionState
Returns the current connection state of the room.
import { useConnectionState } from '@livekit/components-react';
import { ConnectionState } from 'livekit-client';
function ConnectionStatus() {
const connectionState = useConnectionState();
const statusText = {
[ConnectionState.Disconnected]: 'Disconnected',
[ConnectionState.Connecting]: 'Connecting...',
[ConnectionState.Connected]: 'Connected',
[ConnectionState.Reconnecting]: 'Reconnecting...',
};
return <span>{statusText[connectionState]}</span>;
}Connection states
import { ConnectionState } from 'livekit-client';
ConnectionState.Disconnected // Not connected to the room
ConnectionState.Connecting // Connecting to the room
ConnectionState.Connected // Connected to the room
ConnectionState.Reconnecting // Reconnecting after a connection dropOptions
| Option | Type | Description |
|---|---|---|
room | Room | Use a specific room instead of context |
Loading state pattern
function RoomContent() {
const connectionState = useConnectionState();
if (connectionState === ConnectionState.Connecting) {
return <LoadingSpinner />;
}
if (connectionState === ConnectionState.Disconnected) {
return <DisconnectedMessage />;
}
if (connectionState === ConnectionState.Reconnecting) {
return <ReconnectingBanner />;
}
return <MainContent />;
}useRoomInfo
Returns the room's name and metadata.
import { useRoomInfo } from '@livekit/components-react';
function RoomHeader() {
const { name, metadata } = useRoomInfo();
return (
<header>
<h1>{name}</h1>
{metadata && <p>{metadata}</p>}
</header>
);
}Return values
| Property | Type | Description |
|---|---|---|
name | string | Room name |
metadata | `string \ | undefined` |
Parsing metadata
function RoomDetails() {
const { metadata } = useRoomInfo();
const parsedMetadata = metadata ? JSON.parse(metadata) : null;
return (
<div>
{parsedMetadata?.topic && <p>Topic: {parsedMetadata.topic}</p>}
</div>
);
}useLiveKitRoom
Create and manage a LiveKit room instance with connection handling.
import { useLiveKitRoom } from '@livekit/components-react';
function CustomRoomSetup({ token, serverUrl }: { token: string; serverUrl: string }) {
const { room } = useLiveKitRoom({
token,
serverUrl,
connect: true,
audio: true,
video: false,
onConnected: () => console.log('Connected!'),
onDisconnected: () => console.log('Disconnected'),
onError: (error) => console.error('Room error:', error),
});
return (
<RoomContext.Provider value={room}>
<RoomContent />
</RoomContext.Provider>
);
}Options
| Option | Type | Description |
|---|---|---|
token | string | Access token for authentication |
serverUrl | string | LiveKit server URL |
connect | boolean | Whether to connect immediately |
audio | `boolean \ | AudioCaptureOptions` |
video | `boolean \ | VideoCaptureOptions` |
screen | `boolean \ | ScreenShareCaptureOptions` |
options | RoomOptions | Room configuration options |
onConnected | () => void | Callback when connected |
onDisconnected | (reason?: DisconnectReason) => void | Callback when disconnected |
onError | (error: Error) => void | Callback on error |
onMediaDeviceFailure | (failure: MediaDeviceFailure) => void | Callback on device failure |
onEncryptionError | (error: Error) => void | Callback on encryption error |
Return values
| Property | Type | Description |
|---|---|---|
room | `Room \ | undefined` |
useIsRecording
Check if the room is currently being recorded.
import { useIsRecording } from '@livekit/components-react';
function RecordingIndicator() {
const isRecording = useIsRecording();
if (!isRecording) return null;
return (
<div className="flex items-center gap-2 text-red-500">
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse" />
Recording
</div>
);
}useMediaDeviceSelect
Select and manage audio/video input devices.
import { useMediaDeviceSelect } from '@livekit/components-react';
function AudioInputSelector() {
const { devices, activeDeviceId, setActiveMediaDevice } = useMediaDeviceSelect({
kind: 'audioinput',
requestPermissions: true,
onError: (error) => console.error('Device error:', error),
});
return (
<select
value={activeDeviceId}
onChange={(e) => setActiveMediaDevice(e.target.value)}
>
{devices.map((device) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label || `Microphone ${device.deviceId.slice(0, 8)}`}
</option>
))}
</select>
);
}Options
| Option | Type | Description |
|---|---|---|
kind | MediaDeviceKind | Device type: 'audioinput', 'videoinput', or 'audiooutput' |
room | Room | Use a specific room instead of context |
track | `LocalAudioTrack \ | LocalVideoTrack` |
requestPermissions | boolean | Request device permissions to get labels |
onError | (error: Error) => void | Error callback |
Return values
| Property | Type | Description |
|---|---|---|
devices | MediaDeviceInfo[] | Available devices |
activeDeviceId | string | Currently active device ID |
setActiveMediaDevice | (deviceId: string) => Promise<void> | Function to switch devices |
className | string | CSS class for styling |
Video input selector
function VideoInputSelector() {
const { devices, activeDeviceId, setActiveMediaDevice } = useMediaDeviceSelect({
kind: 'videoinput',
});
return (
<select
value={activeDeviceId}
onChange={(e) => setActiveMediaDevice(e.target.value)}
>
{devices.map((device) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label || `Camera ${device.deviceId.slice(0, 8)}`}
</option>
))}
</select>
);
}useAudioPlayback
Control audio playback permissions (for handling browser autoplay restrictions).
import { useAudioPlayback } from '@livekit/components-react';
function AudioPlaybackControl() {
const { canPlayAudio, startAudio } = useAudioPlayback();
if (canPlayAudio) return null;
return (
<button onClick={startAudio}>
Click to enable audio
</button>
);
}Return values
| Property | Type | Description |
|---|---|---|
canPlayAudio | boolean | Whether audio playback is allowed |
startAudio | () => Promise<void> | Function to request audio playback permission |
useStartAudio
Hook for implementing a start audio button (handles browser autoplay policy).
import { useStartAudio } from '@livekit/components-react';
function StartAudioButton() {
const { mergedProps, canPlayAudio } = useStartAudio({
room,
props: {
className: 'start-audio-btn',
},
});
if (canPlayAudio) return null;
return <button {...mergedProps}>Enable Audio</button>;
}useDisconnectButton
Hook for implementing a disconnect button.
import { useDisconnectButton } from '@livekit/components-react';
function LeaveButton() {
const { buttonProps } = useDisconnectButton({
stopTracks: true,
});
return <button {...buttonProps}>Leave Room</button>;
}Options
| Option | Type | Default | Description |
|---|---|---|---|
stopTracks | boolean | true | Stop local tracks when disconnecting |
useToken
Fetch an access token from a token endpoint.
import { useToken } from '@livekit/components-react';
function TokenFetcher({ roomName }: { roomName: string }) {
const token = useToken('/api/token', roomName, {
userInfo: {
identity: 'user-123',
name: 'John Doe',
},
});
if (!token) {
return <div>Fetching token...</div>;
}
return <LiveKitRoom token={token} serverUrl={serverUrl} connect={true} />;
}Options
| Option | Type | Description |
|---|---|---|
userInfo | { identity: string; name?: string; metadata?: string } | User information for token generation |
Token endpoint format
The token endpoint should accept POST requests with:
{
"roomName": "room-name",
"identity": "user-identity",
"name": "User Name",
"metadata": "{}"
}And return:
{
"token": "eyJ..."
}For agent applications
Do not use `useLiveKitRoom` or `LiveKitRoom` for agent applications. Instead, use AgentSessionProvider from livekit-agents-ui, which provides:
- Session management with
TokenSourceauthentication - Automatic audio rendering
- Integration with all Agents UI components
For custom implementations that need low-level control, use useSession from @livekit/components-react instead. The useSession hook provides:
- Managed connection lifecycle (
start,end) - Preconnect audio buffering for better agent responsiveness
- Integration with
useAgentfor agent state management - Token source handling with automatic fetching
See session-hooks.md for hook documentation, or the livekit-agents-ui skill for the recommended component-based approach.
useSequentialRoomConnectDisconnect
Prevents race conditions when connect and disconnect operations overlap during React effect cleanup.
import { useSequentialRoomConnectDisconnect } from '@livekit/components-react';
function RoomWithSafeConnect({ room }: { room: Room }) {
const { connect, disconnect } = useSequentialRoomConnectDisconnect(room);
useEffect(() => {
connect(serverUrl, token);
return () => {
disconnect();
};
}, [connect, disconnect, serverUrl, token]);
return <RoomContent />;
}This hook is useful when:
- You're managing room connection manually
- Your component may unmount while connecting
- You want to prevent "Client initiated disconnect" errors from overlapping operations
Session hooks
Hooks for managing agent sessions with connection lifecycle control. These hooks are marked as beta and may change.
When to use these hooks
The useSession hook is required for using AgentSessionProvider from livekit-agents-ui.
| Approach | When to use |
|---|---|
useSession + AgentSessionProvider (from livekit-agents-ui) | Recommended. Standard approach for all agent apps. Pass the session to AgentSessionProvider, which handles audio rendering. Use Agents UI components for the UI. |
useSession (beta)
Create and manage a LiveKit session with connection lifecycle, token handling, and local track management.
Important: Always use useRef when creating a TokenSource to prevent it from being recreated on each render.
Standard usage with AgentSessionProvider
The most common pattern is to use useSession with AgentSessionProvider from livekit-agents-ui:
import { useRef, useEffect } from 'react';
import { useSession } from '@livekit/components-react';
import { TokenSource, TokenSourceConfigurable } from 'livekit-client';
import { AgentSessionProvider } from '@/components/agents-ui/agent-session-provider';
import { AgentControlBar } from '@/components/agents-ui/agent-control-bar';
function AgentApp() {
// Use useRef to prevent recreating TokenSource on each render
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.sandboxTokenServer('your-sandbox-id')
).current;
// Create session using useSession hook (required for AgentSessionProvider)
const session = useSession(tokenSource, {
agentName: 'your-agent-name',
});
// Auto-start session with cleanup
useEffect(() => {
session.start();
return () => session.end();
}, []);
return (
<AgentSessionProvider session={session}>
<AgentControlBar />
</AgentSessionProvider>
);
}For production, use TokenSource.endpoint():
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.endpoint('/api/token')
).current;
const session = useSession(tokenSource, {
roomName: 'my-room',
participantIdentity: 'user-123',
participantName: 'John',
agentName: 'my-agent',
});Fully custom usage with SessionProvider (not recommended)
Note: This pattern uses UI components from@livekit/components-reactdirectly. For agent applications, useAgentSessionProviderfrom livekit-agents-ui instead, which wraps these components and provides a better developer experience.
For fully custom implementations without Agents UI components, use SessionProvider and RoomAudioRenderer directly:
import { useRef, useEffect } from 'react';
import { useSession, useAgent, SessionProvider, RoomAudioRenderer } from '@livekit/components-react';
import { TokenSource, TokenSourceConfigurable } from 'livekit-client';
function CustomAgentApp() {
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.sandboxTokenServer('your-sandbox-id')
).current;
const session = useSession(tokenSource, {
agentName: 'your-agent-name',
});
const agent = useAgent(session);
useEffect(() => {
session.start();
return () => session.end();
}, []);
return (
<SessionProvider session={session}>
<RoomAudioRenderer />
<div>
<p>Connection: {session.connectionState}</p>
<p>Agent: {agent.state}</p>
</div>
</SessionProvider>
);
}Starting and ending a session
import { UseSessionReturn } from '@livekit/components-react';
function SessionContent({ session }: { session: UseSessionReturn }) {
const handleStart = async () => {
await session.start({
tracks: {
microphone: {
enabled: true,
publishOptions: { preConnectBuffer: true } // Enable audio buffering before agent connects
},
camera: { enabled: false },
},
});
};
const handleEnd = async () => {
await session.end();
};
return (
<div>
<p>State: {session.connectionState}</p>
<p>Connected: {session.isConnected ? 'Yes' : 'No'}</p>
<button onClick={handleStart} disabled={session.isConnected}>
Start
</button>
<button onClick={handleEnd} disabled={!session.isConnected}>
End
</button>
</div>
);
}Return values
| Property | Type | Description |
|---|---|---|
room | Room | The underlying LiveKit room instance |
connectionState | ConnectionState | Current connection state |
isConnected | boolean | Whether session is connected |
local.cameraTrack | `TrackReference \ | undefined` |
local.microphoneTrack | `TrackReference \ | undefined` |
local.screenShareTrack | `TrackReference \ | undefined` |
Methods
| Method | Description |
|---|---|
start(options?) | Connect to room and start the session |
end() | Disconnect from room and end the session |
prepareConnection() | Pre-warm the connection (called automatically) |
waitUntilConnected(signal?) | Promise that resolves when connected |
waitUntilDisconnected(signal?) | Promise that resolves when disconnected |
Session options
interface UseSessionOptions {
room?: Room; // Use existing room instead of creating one
agentConnectTimeoutMilliseconds?: number; // Timeout for agent connection (default: 20000)
// For TokenSourceConfigurable only:
roomName?: string; // Room name for token generation
participantName?: string; // Display name
participantIdentity?: string; // Unique identity
participantMetadata?: string; // Custom metadata
participantAttributes?: Record<string, string>; // Custom attributes
agentName?: string; // Agent name for dispatch
agentMetadata?: string; // Agent metadata
}Start options
interface SessionConnectOptions {
signal?: AbortSignal; // Abort signal for cancellation
tracks?: {
microphone?: {
enabled?: boolean;
publishOptions?: TrackPublishOptions;
};
camera?: {
enabled?: boolean;
publishOptions?: TrackPublishOptions;
};
screenShare?: {
enabled?: boolean;
publishOptions?: TrackPublishOptions;
};
};
roomConnectOptions?: RoomConnectOptions;
}Preconnect audio buffer
By default, session.start() enables the microphone with preConnectBuffer: true. This records user audio before the agent connects, allowing the agent to hear what the user said while waiting for connection.
// Preconnect buffer is enabled by default
await session.start();
// Disable preconnect buffer
await session.start({
tracks: {
microphone: {
enabled: true,
publishOptions: { preConnectBuffer: false }
},
},
});Waiting for connection
import { useRef } from 'react';
import { useSession, useAgent } from '@livekit/components-react';
import { TokenSource, TokenSourceConfigurable } from 'livekit-client';
function SessionWithWait() {
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.sandboxTokenServer('your-sandbox-id')
).current;
const session = useSession(tokenSource, { agentName: 'your-agent' });
const agent = useAgent(session);
const handleStart = async () => {
const controller = new AbortController();
// Set a timeout
setTimeout(() => controller.abort(), 30000);
try {
await session.start({ signal: controller.signal });
await agent.waitUntilConnected(controller.signal);
console.log('Session and agent ready!');
} catch (error) {
console.error('Connection failed or timed out:', error);
}
};
return <button onClick={handleStart}>Start Session</button>;
}Session events
Use useEvents to listen for session events:
import { useRef } from 'react';
import { useSession, useEvents, SessionEvent } from '@livekit/components-react';
import { TokenSource, TokenSourceConfigurable } from 'livekit-client';
function SessionEventHandler() {
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.sandboxTokenServer('your-sandbox-id')
).current;
const session = useSession(tokenSource, { agentName: 'your-agent' });
useEvents(session, SessionEvent.ConnectionStateChanged, (state) => {
console.log('Connection state:', state);
});
useEvents(session, SessionEvent.MediaDevicesError, (error) => {
console.error('Media device error:', error);
});
useEvents(session, SessionEvent.EncryptionError, (error) => {
console.error('Encryption error:', error);
});
return null;
}Complete custom agent app example (not recommended)
Note: This example uses UI components from@livekit/components-reactdirectly. For agent applications, useAgentSessionProviderfrom livekit-agents-ui instead.
For most apps, use useSession with AgentSessionProvider from livekit-agents-ui. The example below shows how to build a fully custom implementation using useSession + SessionProvider when you don't want to use Agents UI components:
import { useRef, useEffect, useState } from 'react';
import { useSession, useAgent, useSessionMessages, SessionProvider, RoomAudioRenderer } from '@livekit/components-react';
import { TokenSource, TokenSourceConfigurable } from 'livekit-client';
function AgentApp() {
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.sandboxTokenServer('your-sandbox-id')
).current;
const session = useSession(tokenSource, { agentName: 'your-agent' });
const agent = useAgent(session);
const { messages, send, isSending } = useSessionMessages(session);
const [input, setInput] = useState('');
useEffect(() => {
session.start();
return () => session.end();
}, []);
const handleSend = async () => {
if (input.trim()) {
await send(input);
setInput('');
}
};
return (
<SessionProvider session={session}>
<RoomAudioRenderer />
<div>
<p>Session: {session.connectionState}</p>
<p>Agent: {agent.state}</p>
</div>
<div>
{messages.map((msg) => (
<div key={msg.id}>
{msg.type === 'userTranscript' && <p>You: {msg.message}</p>}
{msg.type === 'agentTranscript' && <p>Agent: {msg.message}</p>}
{msg.type === 'chat' && <p>{msg.from?.name}: {msg.message}</p>}
</div>
))}
</div>
{session.isConnected && (
<div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
/>
<button onClick={handleSend} disabled={isSending}>
Send
</button>
</div>
)}
</SessionProvider>
);
}TokenSource factory methods
The TokenSource object from livekit-client provides factory methods to create token sources.
Important: Always wrap token source creation in useRef to prevent recreation on each render:
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.sandboxTokenServer('your-sandbox-id')
).current;TokenSource.sandboxTokenServer (development)
Use for development with LiveKit Cloud Sandbox. Create a sandbox at cloud.livekit.io:
import { useRef } from 'react';
import { TokenSource, TokenSourceConfigurable } from 'livekit-client';
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.sandboxTokenServer('your-sandbox-id')
).current;
const session = useSession(tokenSource, {
agentName: 'your-agent-name',
});TokenSource.endpoint (production)
Use for production with your own token endpoint:
import { useRef } from 'react';
import { TokenSource, TokenSourceConfigurable } from 'livekit-client';
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.endpoint('/api/token')
).current;
const session = useSession(tokenSource, {
roomName: 'my-room',
participantIdentity: 'user-123',
participantName: 'John Doe',
agentName: 'my-agent',
});Your token endpoint should accept POST requests with:
{
"room_name": "my-room",
"participant_identity": "user-123",
"participant_name": "John Doe",
"agent_name": "my-agent"
}And return:
{
"serverUrl": "wss://your-project.livekit.cloud",
"participantToken": "eyJ..."
}TokenSource.literal
Use when you already have a token and server URL:
import { useRef } from 'react';
import { TokenSource, TokenSourceFixed } from 'livekit-client';
const tokenSource: TokenSourceFixed = useRef(
TokenSource.literal({
serverUrl: 'wss://your-project.livekit.cloud',
participantToken: token
})
).current;
const session = useSession(tokenSource);TokenSource.custom
Use for custom token fetching logic:
import { useRef } from 'react';
import { TokenSource, TokenSourceConfigurable } from 'livekit-client';
const tokenSource: TokenSourceConfigurable = useRef(
TokenSource.custom(async (options) => {
const response = await fetch('/api/custom-token', {
method: 'POST',
body: JSON.stringify({
room: options.roomName,
user: options.participantIdentity,
}),
});
return response.json();
})
).current;Track hooks
Hooks for working with audio and video tracks in a LiveKit room.
For pre-built media controls, use AgentTrackToggle and AgentControlBar from livekit-agents-ui. For audio visualization, use AgentAudioVisualizerBar, AgentAudioVisualizerGrid, or AgentAudioVisualizerRadial from livekit-agents-ui. Use these hooks when building custom implementations.
useTracks
Returns an array of track references for the specified sources.
import { useTracks } from '@livekit/components-react';
import { Track } from 'livekit-client';
function VideoGrid() {
// Get all camera tracks
const tracks = useTracks([Track.Source.Camera]);
return (
<div className="grid grid-cols-2 gap-4">
{tracks.map((trackRef) => (
<VideoTrack key={trackRef.participant.identity} trackRef={trackRef} />
))}
</div>
);
}Source types
import { Track } from 'livekit-client';
// Available sources
Track.Source.Camera
Track.Source.Microphone
Track.Source.ScreenShare
Track.Source.ScreenShareAudio
Track.Source.UnknownDefault sources
If no sources are provided, returns all track types:
const allTracks = useTracks(); // Camera, Microphone, ScreenShare, ScreenShareAudio, UnknownWith placeholders
Use withPlaceholder to get placeholders for participants without a published track:
const tracksWithPlaceholders = useTracks([
{ source: Track.Source.Camera, withPlaceholder: true },
]);
// Returns TrackReferenceOrPlaceholder[] instead of TrackReference[]Options
| Option | Type | Description |
|---|---|---|
updateOnlyOn | RoomEvent[] | Limit re-renders to specific room events |
onlySubscribed | boolean | Only return subscribed tracks |
room | Room | Use a specific room instead of context |
useParticipantTracks
Returns tracks for a specific participant.
import { useParticipantTracks } from '@livekit/components-react';
import { Track } from 'livekit-client';
function ParticipantMedia({ participantIdentity }: { participantIdentity: string }) {
const tracks = useParticipantTracks(
[Track.Source.Camera, Track.Source.Microphone],
participantIdentity
);
const cameraTrack = tracks.find((t) => t.source === Track.Source.Camera);
const micTrack = tracks.find((t) => t.source === Track.Source.Microphone);
return (
<div>
{cameraTrack && <VideoTrack trackRef={cameraTrack} />}
{micTrack && <AudioTrack trackRef={micTrack} />}
</div>
);
}Usage with participant context
When used inside a ParticipantContext, the identity is optional:
function ParticipantVideo() {
// Uses participant from context
const tracks = useParticipantTracks([Track.Source.Camera]);
// ...
}useTrackToggle
Toggle the publish state of a track source (microphone, camera, screen share).
import { useTrackToggle } from '@livekit/components-react';
import { Track } from 'livekit-client';
function MicrophoneButton() {
const { enabled, pending, toggle, track, buttonProps } = useTrackToggle({
source: Track.Source.Microphone,
});
return (
<button {...buttonProps} disabled={pending}>
{enabled ? 'Mute' : 'Unmute'}
</button>
);
}Options
| Option | Type | Description |
|---|---|---|
source | Track.Source | Track source to toggle (required) |
initialState | boolean | Initial enabled state |
captureOptions | `AudioCaptureOptions \ | VideoCaptureOptions` |
publishOptions | TrackPublishOptions | Options for publishing the track |
onChange | (enabled: boolean, isUserInteraction: boolean) => void | Called when state changes |
onDeviceError | (error: Error) => void | Called on device error |
room | Room | Use a specific room instead of context |
Return values
| Property | Type | Description |
|---|---|---|
enabled | boolean | Whether the track is enabled |
pending | boolean | Whether a toggle operation is in progress |
toggle | (enabled?: boolean) => Promise<void> | Function to toggle the track |
track | `TrackPublication \ | undefined` |
buttonProps | ButtonHTMLAttributes | Props to spread on a button element |
Camera toggle example
function CameraButton() {
const { enabled, toggle, pending } = useTrackToggle({
source: Track.Source.Camera,
captureOptions: {
resolution: { width: 1280, height: 720 },
},
onDeviceError: (error) => {
console.error('Camera error:', error);
},
});
return (
<button onClick={() => toggle()} disabled={pending}>
{enabled ? 'Turn off camera' : 'Turn on camera'}
</button>
);
}useIsMuted
Check if a track is muted.
import { useIsMuted } from '@livekit/components-react';
import { Track } from 'livekit-client';
// With a track reference
function TrackStatus({ trackRef }: { trackRef: TrackReferenceOrPlaceholder }) {
const isMuted = useIsMuted(trackRef);
return <span>{isMuted ? 'Muted' : 'Active'}</span>;
}
// With a source and participant
function ParticipantMicStatus({ participant }: { participant: Participant }) {
const isMuted = useIsMuted(Track.Source.Microphone, { participant });
return <span>{isMuted ? '🔇' : '🔊'}</span>;
}useIsSpeaking
Check if a participant is currently speaking.
import { useIsSpeaking } from '@livekit/components-react';
function SpeakingIndicator({ participant }: { participant: Participant }) {
const isSpeaking = useIsSpeaking(participant);
return (
<div className={isSpeaking ? 'border-green-500' : 'border-gray-500'}>
{participant.name}
{isSpeaking && <span> (speaking)</span>}
</div>
);
}Usage with context
When used inside a ParticipantContext, the participant is optional:
function SpeakingBadge() {
const isSpeaking = useIsSpeaking();
return isSpeaking ? <span className="badge">Speaking</span> : null;
}useTrackVolume
Get the current volume level of an audio track (0-1 range).
import { useTrackVolume } from '@livekit/components-react';
function VolumeIndicator({ audioTrack }: { audioTrack: LocalAudioTrack | RemoteAudioTrack }) {
const volume = useTrackVolume(audioTrack);
return (
<div className="h-4 bg-gray-200 rounded">
<div
className="h-full bg-green-500 rounded"
style={{ width: `${volume * 100}%` }}
/>
</div>
);
}Options
| Option | Type | Default | Description |
|---|---|---|---|
fftSize | number | 32 | FFT size for audio analysis |
smoothingTimeConstant | number | 0 | Smoothing time constant |
useMultibandTrackVolume
Get volume levels across multiple frequency bands for audio visualization.
import { useMultibandTrackVolume } from '@livekit/components-react';
function AudioVisualizer({ audioTrack }: { audioTrack: LocalAudioTrack | RemoteAudioTrack }) {
const frequencyBands = useMultibandTrackVolume(audioTrack, {
bands: 5,
loPass: 100,
hiPass: 600,
});
return (
<div className="flex gap-1 h-16 items-end">
{frequencyBands.map((level, i) => (
<div
key={i}
className="w-2 bg-blue-500"
style={{ height: `${level * 100}%` }}
/>
))}
</div>
);
}Options
| Option | Type | Default | Description |
|---|---|---|---|
bands | number | 5 | Number of frequency bands |
loPass | number | 100 | Low frequency cutoff |
hiPass | number | 600 | High frequency cutoff |
updateInterval | number | 32 | Update interval in ms |
analyserOptions | AnalyserOptions | { fftSize: 2048 } | Web Audio analyser options |
useAudioWaveform
Get waveform data for audio visualization.
import { useAudioWaveform } from '@livekit/components-react';
function Waveform({ audioTrack }: { audioTrack: LocalAudioTrack | RemoteAudioTrack }) {
const { bars } = useAudioWaveform(audioTrack, {
barCount: 120,
volMultiplier: 5,
});
return (
<div className="flex gap-px h-16 items-center">
{bars.map((height, i) => (
<div
key={i}
className="w-0.5 bg-purple-500"
style={{ height: `${Math.min(height * 100, 100)}%` }}
/>
))}
</div>
);
}Options
| Option | Type | Default | Description |
|---|---|---|---|
barCount | number | 120 | Number of bars to display |
volMultiplier | number | 5 | Volume multiplier |
updateInterval | number | 20 | Update interval in ms |
useTrackByName
Get a track by its name property.
import { useTrackByName } from '@livekit/components-react';
function NamedTrack({ trackName, participant }: { trackName: string; participant: Participant }) {
const trackRef = useTrackByName(trackName, participant);
if (!trackRef) {
return <div>Track not found</div>;
}
return <VideoTrack trackRef={trackRef} />;
}