Appearance
React
@talkline/react binds @talkline/core to React: a provider and a set of hooks that re-render when the client's state changes. It is headless — no components, no styles, no text. All the logic is in @talkline/core, which is a peer dependency: install both, with @talkline/protocol (Installing the SDKs).
The provider
Create the client once, outside React's lifecycle — not in an effect, which StrictMode runs twice — and give it to the provider:
tsx
import { useMemo } from 'react';
import { TalklineClient } from '@talkline/core';
import { TalklineProvider } from '@talkline/react';
export function App() {
const client = useMemo(
() =>
new TalklineClient({
url: 'wss://signal.talkline.example',
token: () => fetch('/api/talkline-token', { method: 'POST' }).then((r) => r.json()).then((b) => b.token),
}),
[],
);
return (
<TalklineProvider client={client} connect>
<Calling />
</TalklineProvider>
);
}connect connects on mount. The provider never closes the client: call client.close() yourself when the user signs out.
If that connect fails — a token your endpoint would not give, a server it could not reach — the provider passes the rejection to its onError prop, and to nothing else. Failures no promise was waiting for are the client's error event, which useErrors() collects; the two never carry the same error.
Hooks
| Hook | Returns |
|---|---|
useTalkline() | The client, for calling methods: useTalkline().call({ to: ['agent:7'] }) |
useConnection() | { state, attempt, cause, reconnecting } — Connection and recovery |
useIncomingCall() | The call ringing this client, or null |
useCall() | The current call — placed, ringing (the same call useIncomingCall() returns) or up — and, once it is over, the call that ended until the next one replaces it; null before the first. Re-renders on every change to it |
useRoom() | The room this client is seated in, or null — null again once it leaves, which the end of a call does on both sides |
useParticipants() | Everyone else in the room |
useTracks(participant) | A participant's tracks |
useActiveSpeakers() | Who is speaking, loudest first |
useLocalMedia() | { tracks, micEnabled, cameraEnabled, screenShareEnabled, toggleMic, toggleCamera, toggleScreenShare } |
useVisibleTile(track) | A ref for a remote video element: it attaches the track, and tells the media server how big the element is and whether it is visible. Use it instead of track.ref, not as well |
useAudioPlayback() | { blocked, resume } — blocked when the browser needs a click before it plays audio |
useQuality(identity?) | { level } — this client's connection quality, or a participant's |
useRecording() | { recording, acknowledge } — the room's recording, or null, and the user's consent to it — Recording a room |
useAudioOnlyFallback() | boolean — whether the media server sends this client audio only, its connection too slow for video |
useStats() | The latest media statistics samples |
useDataMessages(limit?) | The latest data messages |
useErrors(limit?) | The latest errors no promise was waiting for |
useDevices() | { audioInput, audioOutput, videoInput, labelsAvailable, refresh } |
Each returns a value that changes only when what it describes changes, so a component re-renders for its own reasons and no others.
A call screen
tsx
import { useState } from 'react';
import { isTalklineError, type RemoteParticipant } from '@talkline/core';
import { useAudioPlayback, useCall, useIncomingCall, useLocalMedia, useParticipants, useRoom, useTracks } from '@talkline/react';
function Calling() {
const incoming = useIncomingCall();
const room = useRoom();
if (incoming) return <Ringing />;
if (room) return <InCall />;
return <Dialer />;
}
function Ringing() {
const call = useIncomingCall()!;
const [failed, setFailed] = useState<string | null>(null);
const answer = () => call.accept().catch((err) => setFailed(isTalklineError(err) ? err.code : 'unknown'));
return (
<div>
<p>{call.from} is calling</p>
<button onClick={answer}>Answer</button>
<button onClick={() => void call.reject()}>Decline</button>
{failed === 'already_answered' && <p>Answered on another device.</p>}
</div>
);
}
function InCall() {
const call = useCall();
const room = useRoom()!;
const people = useParticipants();
const { micEnabled, toggleMic } = useLocalMedia();
const playback = useAudioPlayback();
return (
<div>
{people.map((p) => <Tile key={p.identity} participant={p} />)}
{playback.blocked && <button onClick={() => void playback.resume()}>Turn on sound</button>}
<button onClick={() => void toggleMic()}>{micEnabled ? 'Mute' : 'Unmute'}</button>
<button onClick={() => void (call?.room ? call.hangup() : room.leave())}>Leave</button>
</div>
);
}
function Tile({ participant }: { participant: RemoteParticipant }) {
const tracks = useTracks(participant);
const video = tracks.find((t) => t.kind === 'video' && t.source === 'camera');
const audio = tracks.find((t) => t.kind === 'audio');
return (
<figure>
{video && <video ref={video.ref} playsInline />}
{audio && <audio ref={audio.ref} />}
<figcaption>{participant.displayName || participant.identity}</figcaption>
</figure>
);
}track.ref is a stable ref callback: React attaches the track once and detaches it once, however often the component renders. In a group room, use useVisibleTile(track) for remote video instead, so tiles off screen are paused and small ones get a small video (A group room).
source tells a camera from a screen in a group room. In a one-to-one call nothing says which is which, so there every remote video track is camera and every remote audio track microphone — a screen shared in the call included — and the Tile above shows whichever video arrived.
How a call ended
useCall() keeps returning a call after it ends, until the next call replaces it. Its status is final and its endReason says why, from this side:
tsx
function LastCall() {
const call = useCall();
if (!call || call.endReason === null) return null;
if (call.endReason === 'remote_hangup') return <p>They hung up.</p>;
if (call.endReason === 'no_answer') return <p>No answer.</p>;
return <p>Call ended.</p>;
}The reasons are in the one-to-one call guide. Every hook is in the @talkline/react reference.