CALLBACK_KEYS runtime array of all Callbacks keys, used by the React SDK for callback composition81013c0: Breaking: Input class removed from exports; VoiceConversation.input is now private; changeInputDevice() returns void.
The Input class is no longer exported. The input field on VoiceConversation is now private. changeInputDevice() returns Promise<void> instead of Promise<Input>.
Before:
import { Input } from "@elevenlabs/client";
const input: Input = conversation.input;
input.analyser.getByteFrequencyData(data);
input.setMuted(true);
const newInput: Input = await conversation.changeInputDevice(config);
newInput.worklet.port.postMessage(…);
After:
import type { InputController } from "@elevenlabs/client";
conversation.getInputByteFrequencyData(); // replaces input.analyser.getByteFrequencyData
conversation.setMicMuted(true); // replaces input.setMuted
await conversation.changeInputDevice(config); // return value dropped
Migration:
import { Input } with import type { InputController } if you need the type.conversation.input.analyser.getByteFrequencyData(data) with conversation.getInputByteFrequencyData().conversation.input.setMuted(v) with conversation.setMicMuted(v).changeInputDevice().81013c0: Breaking: InputController and OutputController interfaces are now exported; Input and Output class exports are replaced by these interfaces.
Before:
import { Input, Output } from "@elevenlabs/client";
After:
import type { InputController, OutputController } from "@elevenlabs/client";
81013c0: Breaking: Conversation is no longer a class — it is now a plain namespace object and a type alias for TextConversation | VoiceConversation.
instanceof Conversation no longer compiles. Subclassing Conversation is no longer possible. The startSession() call is unchanged.
Before:
import { Conversation } from "@elevenlabs/client";
// instanceof check compiled fine
if (session instanceof Conversation) { … }
// subclassing was possible
class MyConversation extends Conversation { … }
// startSession returned the class type
const session: Conversation = await Conversation.startSession(options);
After:
import { Conversation } from "@elevenlabs/client";
import type {
Conversation,
TextConversation,
VoiceConversation,
} from "@elevenlabs/client";
// startSession call is unchanged
const session: Conversation = await Conversation.startSession(options);
// Narrow using the concrete types or duck-typing instead of instanceof
if ("changeInputDevice" in session) {
// session is VoiceConversation
}
Migration:
instanceof Conversation checks. Narrow on TextConversation or VoiceConversation using "changeInputDevice" in session (voice) or duck-typing on the methods you need.Conversation — implement the BaseConversation interface directly or compose instead.startSession() call is unchanged and requires no migration.81013c0: Breaking: Output class removed from exports; VoiceConversation.output is now private; changeOutputDevice() returns void.
The Output class is no longer exported. The output field on VoiceConversation is now private. changeOutputDevice() returns Promise<void> instead of Promise<Output>.
Before:
import { Output } from "@elevenlabs/client";
const output: Output = conversation.output;
output.gain.gain.value = 0.5;
output.analyser.getByteFrequencyData(data);
output.worklet.port.postMessage({ type: "interrupt" });
const newOutput: Output = await conversation.changeOutputDevice(config);
After:
import type { OutputController } from "@elevenlabs/client";
conversation.setVolume({ volume: 0.5 }); // replaces output.gain.gain.value
conversation.getOutputByteFrequencyData(); // replaces output.analyser.getByteFrequencyData
// interruption is handled internally by VoiceConversation
await conversation.changeOutputDevice(config); // return value dropped
Migration:
import { Output } with import type { OutputController } if you need the type.conversation.output.gain.gain.value = v with conversation.setVolume({ volume: v }).conversation.output.analyser.getByteFrequencyData(data) with conversation.getOutputByteFrequencyData().changeOutputDevice().81013c0: Breaking: VoiceConversation.wakeLock is now private.
The wakeLock field is no longer accessible on VoiceConversation. It was always an internal detail for preventing screen sleep during a session and was never intended as stable public API.
Before:
const lock: WakeLockSentinel | null = conversation.wakeLock;
if (lock) {
await lock.release();
}
After: Wake lock lifecycle is managed entirely by VoiceConversation. There is no replacement — the lock is released automatically when the session ends. If you need to suppress wake locking entirely, pass useWakeLock: false in the session options.
const conversation = await Conversation.startSession({
// …
useWakeLock: false, // opt out of wake lock management
});
77798c7: Breaking: Complete API rewrite. The custom LiveKit-based implementation (ElevenLabsProvider, useConversation) has been removed and replaced with re-exports from @elevenlabs/react.
The package now provides ConversationProvider and granular hooks (useConversationControls, useConversationStatus, useConversationInput, useConversationMode, useConversationFeedback) instead of the previous monolithic useConversation hook.
On React Native, the package performs side-effects on import: polyfilling WebRTC globals, configuring native AudioSession, and registering a platform-specific voice session strategy. On web, it re-exports without side-effects.
Before:
import {
ElevenLabsProvider,
useConversation,
} from "@elevenlabs/react-native";
function App() {
return (
<ElevenLabsProvider>
<Conversation />
</ElevenLabsProvider>
);
}
function Conversation() {
const conversation = useConversation({
onConnect: ({ conversationId }) =>
console.log("Connected", conversationId),
onError: message => console.error(message),
});
return (
<Button
onPress={() => conversation.startSession({ agentId: "your-agent-id" })}
/>
);
}
After:
import {
ConversationProvider,
useConversationControls,
useConversationStatus,
} from "@elevenlabs/react-native";
function App() {
return (
<ConversationProvider
onConnect={({ conversationId }) =>
console.log("Connected", conversationId)
}
onError={message => console.error(message)}
>
<Conversation />
</ConversationProvider>
);
}
function Conversation() {
const { startSession } = useConversationControls();
const { status } = useConversationStatus();
return (
<Button onPress={() => startSession({ agentId: "your-agent-id" })} />
);
}
cea40aa: Breaking: useConversation now requires a ConversationProvider ancestor. The hook accepts the same options as before and returns the same shape, but must be rendered inside a provider.
New fields on the return value: isMuted, setMuted, isListening, mode, and message.
Removed exports:
DeviceFormatConfig — use FormatConfig from @elevenlabs/client instead.DeviceInputConfig — use InputDeviceConfig from @elevenlabs/client instead.Re-export change: @elevenlabs/react now re-exports all of @elevenlabs/client via export *, replacing the previous selective re-exports.
Wrap your app (or the relevant subtree) in a ConversationProvider. Options can live on the provider, on the hook, or both — the provider merges them.
Before:
import { useConversation } from "@elevenlabs/react";
function App() {
const { status, isSpeaking, startSession, endSession } = useConversation({
agentId: "your-agent-id",
onMessage: message => console.log(message),
onError: error => console.error(error),
});
return (
<div>
<p>Status: {status}</p>
<p>{isSpeaking ? "Agent is speaking" : "Agent is listening"}</p>
<button onClick={() => startSession()}>Start</button>
<button onClick={() => endSession()}>Stop</button>
</div>
);
}
After:
import { ConversationProvider, useConversation } from "@elevenlabs/react";
function App() {
return (
<ConversationProvider>
<Conversation />
</ConversationProvider>
);
}
function Conversation() {
const { status, isSpeaking, startSession, endSession } = useConversation({
agentId: "your-agent-id",
onMessage: message => console.log(message),
onError: error => console.error(error),
});
return (
<div>
<p>Status: {status}</p>
<p>{isSpeaking ? "Agent is speaking" : "Agent is listening"}</p>
<button onClick={() => startSession()}>Start</button>
<button onClick={() => endSession()}>Stop</button>
</div>
);
}
81013c0: Breaking: DeviceFormatConfig and DeviceInputConfig have been removed. Use FormatConfig and InputDeviceConfig from @elevenlabs/client instead.
These were duplicates of types already exported by @elevenlabs/client. changeOutputDevice() now accepts FormatConfig & OutputConfig (previously DeviceFormatConfig & OutputConfig).
Before:
import type {
DeviceFormatConfig,
DeviceInputConfig,
} from "@elevenlabs/react";
await conversation.changeInputDevice({
format: "pcm",
sampleRate: 16000,
inputDeviceId: "my-device",
});
await conversation.changeOutputDevice({
format: "pcm",
sampleRate: 16000,
outputDeviceId: "my-device",
});
After:
import type {
FormatConfig,
InputDeviceConfig,
OutputConfig,
} from "@elevenlabs/client";
await conversation.changeInputDevice({
format: "pcm",
sampleRate: 16000,
inputDeviceId: "my-device",
});
await conversation.changeOutputDevice({
format: "pcm",
sampleRate: 16000,
outputDeviceId: "my-device",
});
Migration: Replace DeviceFormatConfig with FormatConfig and DeviceInputConfig with InputDeviceConfig, both imported from @elevenlabs/client. The runtime values are unchanged — only the type imports need updating.
cea40aa: Add granular conversation hooks for better render performance. Each hook subscribes to an independent slice of conversation state, so a status change won't re-render a component that only uses mode, and vice versa.
New hooks:
useConversationControls() — stable action methods: startSession, endSession, sendUserMessage, setVolume, changeInputDevice, changeOutputDevice, sendContextualUpdate, sendFeedback, sendUserActivity, sendMCPToolApprovalResult, getId, getInputByteFrequencyData, getOutputByteFrequencyData, getInputVolume, getOutputVolume. References are stable across renders and never cause re-renders.useConversationStatus() — reactive status ("disconnected" | "connecting" | "connected" | "error") and optional message.useConversationInput() — reactive isMuted state and setMuted action.useConversationMode() — reactive mode ("speaking" | "listening") with isSpeaking / isListening convenience booleans.useConversationFeedback() — canSendFeedback state and sendFeedback(like: boolean) action.useRawConversation() — escape hatch returning the raw Conversation instance or null.New types: ConversationControlsValue, ConversationStatusValue, ConversationInputValue, ConversationModeValue, ConversationFeedbackValue.
All hooks must be used within a ConversationProvider.
useConversation to granular hooksWith useConversation, every state change re-renders the consuming component. The granular hooks let you split your UI so each component subscribes only to what it needs:
useConversation return value | Granular hook |
|---|---|
status, message | useConversationStatus() |
isSpeaking, isListening, mode | useConversationMode() |
canSendFeedback, sendFeedback | useConversationFeedback() |
isMuted, setMuted | useConversationInput() |
startSession, endSession, setVolume, … | useConversationControls() |
import {
ConversationProvider,
useConversationStatus,
useConversationMode,
useConversationControls,
useConversationInput,
useConversationFeedback,
} from "@elevenlabs/react";
function App() {
return (
<ConversationProvider agentId="your-agent-id">
<StatusBadge />
<Controls />
<MuteButton />
<FeedbackButtons />
<ModeIndicator />
</ConversationProvider>
);
}
/** Only re-renders when status changes. */
function StatusBadge() {
const { status } = useConversationStatus();
return <span className={`badge badge-${status}`}>{status}</span>;
}
/** Never re-renders — controls are stable references. */
function Controls() {
const { startSession, endSession } = useConversationControls();
return (
<div>
<button onClick={() => startSession()}>Start</button>
<button onClick={() => endSession()}>Stop</button>
</div>
);
}
/** Only re-renders when mute state changes. */
function MuteButton() {
const { isMuted, setMuted } = useConversationInput();
return (
<button onClick={() => setMuted(!isMuted)}>
{isMuted ? "Unmute" : "Mute"}
</button>
);
}
/** Only re-renders when feedback availability changes. */
function FeedbackButtons() {
const { canSendFeedback, sendFeedback } = useConversationFeedback();
if (!canSendFeedback) return null;
return (
<div>
<button onClick={() => sendFeedback(true)}>👍</button>
<button onClick={() => sendFeedback(false)}>👎</button>
</div>
);
}
/** Only re-renders when mode changes. */
function ModeIndicator() {
const { isSpeaking, isListening } = useConversationMode();
return (
<p>
{isSpeaking
? "Agent is speaking..."
: isListening
? "Listening..."
: ""}
</p>
);
}
cfea047: Add useConversationClientTool hook for dynamically registering client tools from React components.
Tools added or removed after session start are immediately visible to BaseConversation at call time, since it performs dynamic property lookup on the same object reference. A fresh clientTools object is created per startSession call, merging option-provided tools with hook-registered tools. Duplicate tool names (hook-vs-hook or hook-vs-option) are detected and throw an error.
The hook accepts an optional ClientTools type parameter — an interface mapping tool names to function signatures — enabling type-safe tool name constraints and handler param/return inference.
New hook:
useConversationClientTool(name, handler) — registers a client tool that the agent can invoke, automatically cleaning up on unmount.New types: ClientTool, ClientTools, ClientToolResult.
// Untyped — parameters are Record<string, unknown>
useConversationClientTool("get_weather", params => {
return `Weather in ${params.city} is sunny.`;
});
// Type-safe — tool names are constrained, params and return types are inferred
type Tools = {
get_weather: (params: { city: string }) => string;
set_volume: (params: { level: number }) => void;
};
useConversationClientTool<Tools>("get_weather", params => {
// params: { city: string }, must return string
return `Weather in ${params.city} is sunny.`;
});
ConversationStatus type alias.