React Integration
Build React applications with the Talker SDK using hooks and components.
useTalker Hook
A complete React hook for managing Talker SDK state:
hooks/useTalker.ts
import { useEffect, useState, useCallback, useRef } from 'react';
import { TalkerClient, LogLevel } from '@talker-network/talker-sdk';
import type {
ConnectionStatus,
BroadcastStartEvent,
Channel,
} from '@talker-network/talker-sdk';
interface Credentials {
userAuthToken: string;
userId: string;
a_username: string;
a_password: string;
}
interface UseTalkerReturn {
connectionStatus: ConnectionStatus;
isConnected: boolean;
isTalking: boolean;
activeBroadcast: BroadcastStartEvent | null;
channels: Channel[];
startTalking: (channelId: string) => Promise;
stopTalking: () => Promise;
loadChannels: (workspaceId?: string) => Promise;
}
export function useTalker(credentials: Credentials | null): UseTalkerReturn {
const [connectionStatus, setConnectionStatus] = useState('disconnected');
const [isTalking, setIsTalking] = useState(false);
const [activeBroadcast, setActiveBroadcast] = useState(null);
const [channels, setChannels] = useState([]);
const clientRef = useRef(null);
// Initialize client when credentials are available
useEffect(() => {
if (!credentials) return;
const client = new TalkerClient({
userAuthToken: credentials.userAuthToken,
userId: credentials.userId,
a_username: credentials.a_username,
a_password: credentials.a_password,
loggerConfig: {
level: LogLevel.DEBUG,
enableConsole: true,
},
});
clientRef.current = client;
// Connection events
client.on('connection_change', ({ status }) => {
setConnectionStatus(status);
});
// Broadcast events
client.on('broadcast_start', (data) => {
setActiveBroadcast(data);
});
client.on('broadcast_end', () => {
setActiveBroadcast(null);
});
// Cleanup on unmount
return () => {
client.disconnect();
clientRef.current = null;
};
}, [credentials]);
const startTalking = useCallback(async (channelId: string) => {
if (!clientRef.current) return;
const result = await clientRef.current.startTalking(channelId);
if (result.success) {
setIsTalking(true);
}
}, []);
const stopTalking = useCallback(async () => {
if (!clientRef.current) return;
await clientRef.current.stopTalking();
setIsTalking(false);
}, []);
const loadChannels = useCallback(async (workspaceId?: string) => {
if (!clientRef.current) return;
const channelList = await clientRef.current.getChannels(workspaceId);
setChannels(channelList);
}, []);
return {
connectionStatus,
isConnected: connectionStatus === 'connected',
isTalking,
activeBroadcast,
channels,
startTalking,
stopTalking,
loadChannels,
};
}
Using the Hook
App.tsx
import { useState, useEffect } from 'react';
import { useTalker } from './hooks/useTalker';
function App() {
const [credentials, setCredentials] = useState(null);
// Fetch credentials from your backend
useEffect(() => {
fetch('/api/auth', { method: 'POST' })
.then(res => res.json())
.then(setCredentials);
}, []);
const {
connectionStatus,
isConnected,
isTalking,
activeBroadcast,
startTalking,
stopTalking,
} = useTalker(credentials);
return (
<div className="app">
<div className={`status ${connectionStatus}`}>
{connectionStatus}
</div>
{activeBroadcast && (
<div className="broadcast-indicator">
{activeBroadcast.senderName} is speaking
</div>
)}
<button
className={`ptt-button ${isTalking ? 'talking' : ''}`}
onMouseDown={() => startTalking('channel-id')}
onMouseUp={stopTalking}
onMouseLeave={stopTalking}
disabled={!isConnected}
>
{isTalking ? 'Release to Stop' : 'Hold to Talk'}
</button>
</div>
);
}
PTT Button Component
components/PTTButton.tsx
import { useCallback, useRef } from 'react';
interface PTTButtonProps {
channelId: string;
onStartTalking: (channelId: string) => Promise;
onStopTalking: () => Promise;
isTalking: boolean;
isConnected: boolean;
disabled?: boolean;
}
export function PTTButton({
channelId,
onStartTalking,
onStopTalking,
isTalking,
isConnected,
disabled = false,
}: PTTButtonProps) {
const isPressed = useRef(false);
const handleStart = useCallback(async () => {
if (isPressed.current || disabled || !isConnected) return;
isPressed.current = true;
await onStartTalking(channelId);
}, [channelId, onStartTalking, disabled, isConnected]);
const handleStop = useCallback(async () => {
if (!isPressed.current) return;
isPressed.current = false;
await onStopTalking();
}, [onStopTalking]);
return (
<button
className={`ptt-button ${isTalking ? 'talking' : ''} ${!isConnected ? 'disconnected' : ''}`}
onMouseDown={handleStart}
onMouseUp={handleStop}
onMouseLeave={handleStop}
onTouchStart={(e) => {
e.preventDefault();
handleStart();
}}
onTouchEnd={(e) => {
e.preventDefault();
handleStop();
}}
onTouchCancel={handleStop}
disabled={disabled || !isConnected}
>
{!isConnected ? 'Connecting...' : isTalking ? 'Speaking...' : 'Push to Talk'}
</button>
);
}
Channel List Component
components/ChannelList.tsx
import type { Channel } from '@talker-network/talker-sdk';
interface ChannelListProps {
channels: Channel[];
selectedChannel: string | null;
onSelectChannel: (channelId: string) => void;
}
export function ChannelList({
channels,
selectedChannel,
onSelectChannel,
}: ChannelListProps) {
return (
<div className="channel-list">
<h3>Channels</h3>
{channels.map(channel => (
<button
key={channel.channelId}
className={`channel-item ${channel.channelId === selectedChannel ? 'selected' : ''}`}
onClick={() => onSelectChannel(channel.channelId)}
>
<span className="channel-name">{channel.channelName}</span>
<span className="channel-type">{channel.channelType}</span>
</button>
))}
</div>
);
}
Broadcast Indicator Component
components/BroadcastIndicator.tsx
import type { BroadcastStartEvent } from '@talker-network/talker-sdk';
interface BroadcastIndicatorProps {
broadcast: BroadcastStartEvent | null;
}
export function BroadcastIndicator({ broadcast }: BroadcastIndicatorProps) {
if (!broadcast) return null;
return (
<div className="broadcast-indicator">
<div className="pulse"></div>
<span className="sender-name">{broadcast.senderName}</span>
<span className="speaking-text">is speaking</span>
</div>
);
}
Connection Status Component
components/ConnectionStatus.tsx
import type { ConnectionStatus } from '@talker-network/talker-sdk';
interface ConnectionStatusProps {
status: ConnectionStatus;
}
export function ConnectionStatusBadge({ status }: ConnectionStatusProps) {
const statusConfig = {
connected: { color: '#10b981', label: 'Connected' },
connecting: { color: '#f59e0b', label: 'Connecting...' },
disconnected: { color: '#ef4444', label: 'Disconnected' },
};
const config = statusConfig[status];
return (
<div
className="connection-status"
style={{ backgroundColor: config.color }}
>
{config.label}
</div>
);
}
useAudioLevel Hook
hooks/useAudioLevel.ts
import { useEffect, useState, useRef } from 'react';
import type { TalkerClient } from '@talker-network/talker-sdk';
export function useAudioLevel(
client: TalkerClient | null,
isActive: boolean
): number {
const [level, setLevel] = useState(0);
const frameRef = useRef();
useEffect(() => {
if (!client || !isActive) {
setLevel(0);
return;
}
function update() {
setLevel(client!.getAudioLevel());
frameRef.current = requestAnimationFrame(update);
}
update();
return () => {
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
}
};
}, [client, isActive]);
return level;
}
Audio Meter Component
components/AudioMeter.tsx
interface AudioMeterProps {
level: number; // 0-1
}
export function AudioMeter({ level }: AudioMeterProps) {
const percentage = Math.round(level * 100);
// Color based on level
let color = '#64748b'; // Gray
if (level > 0.8) {
color = '#ef4444'; // Red - too loud
} else if (level > 0.1) {
color = '#10b981'; // Green - good
}
return (
<div className="audio-meter">
<div
className="audio-meter-fill"
style={{
width: `${percentage}%`,
backgroundColor: color,
}}
/>
</div>
);
}
Complete React App Example
App.tsx
import { useState, useEffect } from 'react';
import { useTalker } from './hooks/useTalker';
import { PTTButton } from './components/PTTButton';
import { ChannelList } from './components/ChannelList';
import { BroadcastIndicator } from './components/BroadcastIndicator';
import { ConnectionStatusBadge } from './components/ConnectionStatus';
import './App.css';
function App() {
const [credentials, setCredentials] = useState(null);
const [selectedChannel, setSelectedChannel] = useState(null);
// Fetch credentials on mount
useEffect(() => {
async function authenticate() {
const res = await fetch('/api/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'User' }),
});
const creds = await res.json();
setCredentials(creds);
}
authenticate();
}, []);
const {
connectionStatus,
isConnected,
isTalking,
activeBroadcast,
channels,
startTalking,
stopTalking,
loadChannels,
} = useTalker(credentials);
// Load channels when connected
useEffect(() => {
if (isConnected) {
loadChannels();
}
}, [isConnected, loadChannels]);
// Select first channel by default
useEffect(() => {
if (channels.length > 0 && !selectedChannel) {
setSelectedChannel(channels[0].channelId);
}
}, [channels, selectedChannel]);
return (
<div className="app">
<header>
<h1>Talker Demo</h1>
<ConnectionStatusBadge status={connectionStatus} />
</header>
<main>
<aside>
<ChannelList
channels={channels}
selectedChannel={selectedChannel}
onSelectChannel={setSelectedChannel}
/>
</aside>
<section className="chat-area">
<BroadcastIndicator broadcast={activeBroadcast} />
<div className="ptt-container">
{selectedChannel && (
<PTTButton
channelId={selectedChannel}
onStartTalking={startTalking}
onStopTalking={stopTalking}
isTalking={isTalking}
isConnected={isConnected}
/>
)}
</div>
</section>
</main>
</div>
);
}
export default App;
CSS Styles
App.css
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid #e2e8f0;
}
main {
display: flex;
flex: 1;
}
aside {
width: 280px;
border-right: 1px solid #e2e8f0;
padding: 16px;
}
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
}
.ptt-button {
width: 200px;
height: 200px;
border-radius: 50%;
border: none;
background: #3b82f6;
color: white;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.ptt-button:hover {
background: #2563eb;
}
.ptt-button.talking {
background: #ef4444;
transform: scale(1.1);
}
.ptt-button.disconnected {
background: #94a3b8;
cursor: not-allowed;
}
.broadcast-indicator {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 24px;
background: #f0fdf4;
border-radius: 24px;
margin-bottom: 24px;
}
.broadcast-indicator .pulse {
width: 12px;
height: 12px;
background: #10b981;
border-radius: 50%;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.2); }
}
.connection-status {
padding: 6px 12px;
border-radius: 16px;
color: white;
font-size: 12px;
font-weight: 500;
}
.channel-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.channel-item {
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: white;
cursor: pointer;
text-align: left;
}
.channel-item.selected {
border-color: #3b82f6;
background: #eff6ff;
}
.audio-meter {
width: 100%;
height: 8px;
background: #e2e8f0;
border-radius: 4px;
overflow: hidden;
}
.audio-meter-fill {
height: 100%;
transition: width 0.1s;
}